希尔排序(shellsort)又叫增量递减(diminishing increment)排序Q是?/font>D.L. Shell发明的,q个法是通过一个逐渐减小的增量一个数l逐渐近于有序从而达到排序的目的Q该法?/font>1959q公布?/font>
最差时间复杂度Q根据步长序列的不同而不同?/font>已知最好的: O(nlog2n)
最优时间复杂度Q?/font>O(n)
q_旉复杂度:Ҏ步长序列的不同而不同?/font>
原始的算法实现在最坏的情况下需要进?/font>O(n2)的比较和交换?/font>V. Pratt的书[1] 对算法进行了量修改Q可以得性能提升?/font>O(n log2 n)。这比最好的比较法?/font>O(n log n)要差一些?/font>
希尔排序通过比较的全部元素分ؓ几个区域来提升插入排序的性能。这样可以让一个元素可以一ơ性地朝最l位|前q一大步。然后算法再取越来越大的步长q行排序Q算法的最后一步就是普通的插入排序Q但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快Q?/font>
假设有一个很的数据在一个已按升序排好序的数l的末端。如果用复杂度ؓO(n2)的排序(冒排序或插入排序)Q可能会q行nơ的比较和交换才能将该数据移x位|。而希排序会用较大的步长Ud数据Q所以小数据只需q行数比较和交换即可到正确位置?/font>
一个更好理解的希尔排序实现Q将数组列在一个表中ƈ对列排序Q用插入排序Q。重复这q程Q不q每ơ用更长的列来进行。最后整个表只有一列了。将数组转换臌是ؓ了更好地理解q算法,法本n仅仅对原数组q行排序Q通过增加索引的步长,例如是用i += step_size而不?/font>i++Q?/font>
例如Q假设有q样一l数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ]Q如果我们以步长?/font>5开始进行排序,我们可以通过这列表攑֜?/font>5行的表中来更好地描述法Q这样他们就应该看v来是q样Q?/font>
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们Ҏ行进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
当我们以单行来读取数据时我们得到Q?/font>[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].q时10已经U至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变ؓQ?/font>
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长q行排序Q此时就是简单的插入排序了)?/font>
步长的选择是希排序的重要部分。只要最l步长ؓ1M步长序列都可以工作。算法最开始以一定的步长q行排序。然后会l箋以一定步长进行排序,最l算法以步长?/font>1q行排序。当步长?/font>1Ӟ法变ؓ插入排序Q这׃证了数据一定会被排序?/font>
法如下
#include <stdio.h>
void output_array(int data[], int n)
{
}
void swap(int *a, int *b)
{
}
void insertion_sort(int data[], int n, int increment)
{
}
void shellsort(int data[], int n)
{
}
int main()
{
}
插入排序 对基本有序的数组效果非常好,但是对于通常情况则表C般。假设最的数字在最双Q升序排序时Q这个数则要l过nơ交换比较换到最左边。希排序则是对插入排序的很好的修正。而且在希排序很出现最坏状c?/p>
希尔排序通过Ҏl?以一定间隔相隔的位置 q行插入排序Q以辑ֈ让数据快速出现在它应该出现的位置的周_使数l逐步接近基本有序。随着间隔的减,数组来接q基本有序,最后间隔ؓ1Ӟ变成标准的插入排序?/p>
数据的间隔有多种法Q一般要求间隔序列之间互质,此处使用Kunth序列Qh = h * 3 + 1
希尔排序的时间效率很难从理论上证明,实验表明大约是O(n^(3/2)) ~ O(n^(7/6))之间?/p>
代码如下Q?/p>
class Shell { public static void main(String[] args) { int[] a = {9,8,7,6,5,4,3,2,1}; sort(a); println(a); } private static void println(int[] a) { for(int i: a) System.out.print(i + " "); System.out.println(); } private static void sort(int[] a) { int h = 1; while(h <= a.length/3) h = h * 3 + 1; //产成Kunth序列 while(h > 0) { for(int i = h; i < a.length; i++) { //Ҏ个数据进行间隔ؓh的插入排? int pos = i; int temp = a[i]; while(pos >= h && a[pos - h] > temp) { a[pos] = a[pos-h]; pos -= h; } a[pos] = temp; } h = (h - 1) / 3; //减小间隔? } } }
在我们编E的世界里数据的基本l织可以说有三种形式?/p>
其他M的数据组lŞ式都可以看作是这三种数据l织形式的组合变体?br>
l构?或对?可以是基本数据类型或者其他结构体(或对?的组合。结构体或对象一般用来描qC个复杂数据实体?/p>
数组一般是一l同cd的变量的集合Q在内存中表Cؓ一片连l的I间Q因为空间是q箋的,且每一个数据单元占的内存空间的大小是相{的Q所以可以根据地址的偏Ud数据元素实现快速访问,但是当需要插入或者删除一个元素的时候,则需要对目标元素的之后的所有元素进行移动了?/p>
链表的单个节点一般ؓl构体或者对象,因ؓ链表的单个节炚w了需要保存数据之外还需要维护它的相邻节点的关系Q如果想获得链表中的某个节点的|需要从链表的头l点开始遍历,直到扑ֈ需要的东西Q而插入或者删除某个节点的话,需要找到相应的节点Q修改其以及其相邻节点的相关指针的引用即可?/p>
像其他的数据l构Q比?队列Q栈Q树Q都可以通过数组或者链表来l织Qƈ实现相应的操作功能?/p>
?Hash Table
q个世界上没有十全十的东西Q所以我们要学会取舍。Q何技术的实现都没有最好的只要最合适的Q也p实现的最x案是和应用场景息息相关的?br>很多时候,我们惛_数据q行快速的存取Q比如缓存的实现Q,q用一个key来标记自己存取的数据。我们可以把它叫做key-value的结构?br>说到“快?#8221;我们很快惛_数组Q因为数l可以在O(1)的时间复杂内完成指定位置元素的读写操作?br>所以在理想状态,如果一个数l够长Q且存在一个函数可以将每一个key映射到唯一的一个数l下标,那么我们可以很完美的解决问题。但往往资源都是有限的,我们没有那么大的I间Q也不能设计一个无比负责的映射法保证每一个key对应C个唯一的数l下标。所以我们会选择一些折中的Ҏ?/p>
hash table便是册c问题而存在的?/p>
1.哈希函数
Hash或者你可以译成散列或者杂凑,hash操作其本质上是一个数据映成另一个数据,通常情况下原数据的长度比hash后的数据定w大?br>q种映射的关pL们叫做哈希函数?br>
一般情况下 哈希函数的输入可能的L要远q多于哈希值所能表C的LQ所以就有可能两个不同的输入对应同一个哈希|通常把具有不同关键码而具有相同哈希值的记录UC“同义?#8221;?br>在信息安全领域中也经怋用到哈希函数Q不q需要用的是单向哈希函敎ͼ是无法通过哈希的结果反推出输入Q所以经常应用于密码的加密,传输内容的完整性检查,在安全领域常用的哈希法?MD5QSHA1{?br>在哈希表的应用中Q哈希函数常用余数法q行Q也是通过求模的方式算出哈希倹{?/p>
2.哈希?/h3>
哈希表是一U数据结构,实现key-value的快速存取。之前说q数l可以实现快速存取,所以哈希表肯定会用到数组。在q里Q我们把每一个数l的单元叫做一个bucketQ桶Q?/p>
构造哈希函?/h5>
q里哈希函数的作用就是将key映射C个存储地址。所以构造一个哈希表我们得先构造哈希函数?br>如果一个key哈希后对应地址中已l存放了gQ这U情冉|们叫做哈希冲H(Hash collisionsQ?br>如果存在一个哈希函敎ͼ使得每一个输入都能对应到唯一的一个存储单元中Q没有冲H)Q那么这L哈希函数我们可以叫它完美哈希函数QPerfect Hash FunctionQ简UPHF)?br>但ؓ了哈希函数简单,q行速度快,往往不会使用完美哈希函数。所以冲H肯定会存在的,Z减少冲突Q我们希望哈希函数的l果均匀的分布在地址单元的空间中。这样可以有效的减少冲突?br>
装填因子Load factor a=哈希表的实际元素数目(n)/ 哈希表的定w(m) a大Q哈希表冲突的概率越大,但是a接q?Q那么哈希表的空间就浪贏V?br>一般情况下Load factor的gؓ0-0.7QJava实现的HashMap默认的Load factor的gؓ0.75Q当装蝲因子大于q个值的时候,HashMap会对数组q行扩张臛_来两倍大?/p>
冲突解决
既然冲突不可避免Q那么我们就必须对冲H进行解?M能把之前的内容覆盖掉?,
解决冲突的方式主要分两类
开攑֮址?Open addressing)q种Ҏ是在计一个key的哈希的时候,发现目标地址已经有gQ即发生冲突了,q个时候通过相应的函数在此地址后面的地址LQ直到没有冲Hؓ止。这个方法常用的有线性探,二次探测Q再哈希?br>q种解决Ҏ有个不好的地方就是,当发生冲H之后,会在之后的地址I间中找一个放q去Q这样就有可能后来出C个key哈希出来的结果也正好是它放进ȝq个地址I间Q这样就会出现非同义词的两个key发生冲突?br>
链接?Separate chaining)链接法是通过数组和链表组合而成的。当发生冲突的时候只要将其加到对应的链表中即可?/p>
与开攑֮址法相比,链接法有如下几个优点Q?br>①链接法处理冲突单,且无堆积现象Q即非同义词决不会发生冲H,因此q_查找长度较短Q?br>②由于链接法中各链表上的l点I间是动态申LQ故它更适合于造表前无法确定表长的情况Q?br>③开攑֮址法ؓ减少冲突Q要求装填因?#945;较小Q故当结点规模较大时会浪费很多空间。而链接法中可?#945;≥1Q且l点较大Ӟ拉链法中增加的指针域可忽略不计,因此节省I间Q?br>④在用链接法构造的散列表中Q删除结点的操作易于实现。只要简单地删去链表上相应的l点卛_。而对开攑֜址法构造的散列表,删除l点不能单地被删结点的I间|ؓI,否则截断在它之后填人散列表的同义词l点的查找\径。这是因为各U开攑֜址法中Q空地址单元(卛_攑֜址)都是查找p|的条件。因此在 用开攑֜址法处理冲H的散列表上执行删除操作Q只能在被删l点上做删除标记Q而不能真正删除结炏V?/p>
当然链接法也有其~点Q拉链法的缺ҎQ指针需要额外的I间Q故当结点规模较时Q开攑֮址法较省空_而若节省的指针I间用来扩大散列表的规模Q可使装填因子变,q又减少了开攑֮址法中的冲H,从而提高^均查N度?/p>