《算法》第四版
一:排序算法
排序算法简而言之,可以按照时间复杂度分为两种。时间复杂度是指当排序的数据规模曾指数增长时,排序算法所耗费的时间是呈指数级别增长,还是常数级别增长,还是lg级别增长。因此这两种时间复杂度分别就是O(n平方)和O(nlg(n))两种。当需要排序的数据达到一定规模时,这两种排序算法消耗的时长,也就判若云泥。
(一):n的平方级别
(1)选择排序
选择排序主要是在[4,2,7,1,8,9,3]数组的遍历中,寻找到[i
,n)的最小值。如果是希望升序排列的话。然后将最小值与i进行交换。
也就是说,在每次遍历时,只需要交换一次即可。
private void selectSort(int [] arr,int size){
for(int i=0;iarr[j]){
//当发现有更小的值时,更换minIndex。
minIndex=j;
}
}
//将最小值与i的值进行交换。实现选择排序。
swap(arr,i,minIndex);
}
}
因此选择排序,也可以认为是在当前元素及其之后,选择最小值排序。
(2) 插入排序之交换排序
同样的数组[4,2,7,1,8,9,3],如果希望是升序排列的话。插入排序是指,在[0,i]中寻找i的合适位置。如果寻找到了合适位置j,则交换i和j,直到j的位置为0。由于[0,i)已经是有序的啦,因此当首次不满足arr[i]
private void insertSort(int [] arr,int size){
//这里从i=1开始,是因为0号索引位置的元素无法再与它之前的元素比较。
for(int i=1;i0;j--){
//如果寻找到了一次合适位置,就交换一次位置。直到(j-1)为0为止。也就是在[i,0]区间中进行了所有元素与i的比较。
if(arr[j]
一般来说,交换一次两个元素需要三次操作,尽管插入排序可以提前结束内层循环,但是由于上面的插入排序可能会使用多次交换,因此在总的时间耗费上,仍肯能高于选择排序。
(2)插入排序之赋值排序
由于交换是一个相对耗时的操作,因此可以采用赋值比较的方式,优化上面的插入排序。将[i,0]的数组中,i的值保存在一个变量中,然后通过这个变量与arr[j-1]进行比较,如果这个变量小于了arr[j-1],那么就将arr[j]的值赋值为arr[j-1],反之,则(j)这个索引,就是所要求的的位置。
private void insertSort2(int [] arr,int size){
//这里只需要从索引1开始比较,因为索引0之前没有元素了。
for(int i=1;i0;j--){
if(temp
通过赋值来进行比较的插入排序,就不再需要交换元素了。只是额外多需要了两个临时空间,用来存储临时变量。并且依然可以提前结束内层循环。插入排序在排列近乎有序的数组时,几乎可以达到O(n)级别。
(3)希尔排序。
希尔排序是插入排序的延伸。插入排序每次只跟前面的一个元素进行比较,希尔排序则是先将数组按照步长分为逻辑上的若干个小数组,分别进行插入排序。排序后,再继续减小步长的值,再次分组,再次分别使用插入排序。最后再对整个数组进行一次插入排序。关于步长序列的选择,直接影响了希尔排序的最坏情况。比如[1,2,4,8...]最坏的情况是n的平方。[1,3,7...((2的n次方)-1)]这样的序列最坏情况就是n的(1.5次方)。Sedgewick提出的[1,5,19,41,109...]则可以将最坏情况降低为n的(1.3次方)。
public void shellSort(int [] arr,int size){
for(int step=size/2;step>0;step/=2){
for(int i=step;i=0&&arr[j]
(4)归并排序
归并排序是O(nlg(n))级别的算法,相对上面几种排序,归并排序通过递归来实现。先是将数组一分为二,然后各自再一分为二,直到剩下只有一个元素。然后再分别对每个小数组进行排序。排序完成后,两个有序的小数组,再合并为一个有序的大数组。依次递归,最终完成整个数组的排序。排序过程中需要额外的一个数组的空间。
private void merge1(int [] arr,int size){
merge2(arr,0,size-1);
}
private void merge2(int [] arr,int l,int r){
if(l>=r){
return;
}
int middle=l/2+r/2;
merge2(arr,l,middle);
merge2(arr,middle+1,r);
merge3(arr,l,middle,r);
}
private void merge3(int [] arr,int l,int m,int r){
int [] temp = new int[r-l+1];
for(int i=l;i<=r;i++){
temp[i-l]=arr[i];
}
int i=l;
int j=m+1;
for(int k=l;k<=r;k++){
if(j>r){
arr[k]=temp[i-l];
i++;
}else if(i>m){
arr[k]=temp[j-l];
j++;
}else if(temp[i-l]>temp[j-l]){
arr[k]=temp[j-l];
j++;
}else{
arr[k]=temp[i-l];
i++;
}
}
}
(5)快速排序
快速排序也是基于递归来实现,但是快速排序与归并的区别在于,归并在每次二分数组的时候,都可以平衡地将数组一分为二,也就保障了数组在分到最后一层的时候,只有lg(n)的层数。但是快速排序在选定一个元素作为中间值,并且以这个元素来二分数组,大于这个元素的值放在该值的右边,小于这个元素的值放在该元素的左边,如果遇到了有序数组的话。那么这种二分就会导致数组的层级为n,快速排序的时间复杂度也就退化为O(n的平方)。避免这种情况,就是每次在选取元素时,都采用随机选取的方式,这样就将O(n的平方)这种复杂度出现的几率,降低到几乎为0。但是如果遇到大量重复元素出现的数组,还是会让快速排序降低为O(n的平方),此时就需要通过三路快排的方式,保障数组在平分时,能够维持在lg(n)的层级。
public void quickSort(int [] arr,int size){
quickSort1(arr,0,size-1);
}
public void quickSort1(int [] arr,int l,int r){
if(l>=r){
return;
}
int p = partition(arr,l,r);
quickSort1(arr,l,p-1);
quickSort1(arr,p+1,r);
}
public int partition(int [] arr,int l,int r){
//这里应该随机在[l,r]之间选取一个元素作为v的值。不然遇到有序数组的情况,时间复杂度就会退化为n的平方。
int v=arr[l];
int j=l;
for(int k=l+1;k<=r;k++){
if(arr[k]
(6)三路快排
private static void quickThree1(int arr [],int size){
quickThree2(arr,0,size-1);
}
private static void quickThree2(int [] arr,int l,int r){
if(l>=r){
return;
}
int v=arr[l];
int lt=l;
int gt=r+1;
int i=l+1;
while (iv){
swap(arr,i,gt-1);
gt--;
}else{
i++;
}
}
swap(arr,l,lt);
quickThree2(arr,l,lt-1);
quickThree2(arr,gt,r);
}
二:堆
堆又称为优先队列,但是它并不是队列。为了实现一个堆,我们需要定义两个条件。1,任意节点的优先级不得小于子节点的优先级。2,是一个完全二叉树。如果这棵树有n层,那么(n-1)层必须填满,并且n层的元素,从左往右按顺序排开。堆的主要操作,1,插入一个新元素,2,删除最小元素,如果这是个最小堆的话。如果是插入新元素的话,新建的节点位于第n层,然后,该元素与它的父元素比较,如果优先级高于父元素,那么就交换二者的位置。如果是删除根元素的话,那么需要将第n层的最后一个元素,放到根元素的位置,然后根元素的与子元素进行比较,如果比子节点的元素大,则交换,直到最终不能交换为止。