排序
冒泡排序
插入排序
希尔排序
选择排序
快速排序
归并排序
堆排序
桶排序
排序算法的稳定性:排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
冒泡(稳定) :冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
插入(稳定): 插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
希尔(不稳定):希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
选择(不稳定):选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
快速(不稳定):快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
归并(稳定):归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
堆排序(不稳定):我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
快速排序、归并排序、堆排序:
1、快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。
(1) 如果不多于1个数据,直接返回。
(2) 一般选择序列最左边的值作为支点数据。
(3) 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
(4) 对两边利用递归排序数列。
快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。
2、归并排序(MergeSort)
归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
3、堆排序(HeapSort)
堆排序适合于数据量非常大的场合(百万数据)。
堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。
堆排序会将所有的数据建成一个堆,时间复杂度为O(n)。最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
空间复杂度:快排O(logn),堆O(1),归并O(n)
问:快排和堆排序的时间复杂度一样,为什么使用快排的时候多一些?
(1)虽然都是O(nlogn),但堆排序的时间常数比快排要大一些。堆排序最差时间也是O(nlogn),这点比快排好些。
(2)堆排序时,有一个步骤将堆顶的元素和最后一个元素对调,而最后一个元素一般会远远小于顶元素的左右两个孩子,所以会有大量这种近乎无效的比较。
(3)堆排序比较的几乎都是不相邻的元素,没有利用缓存cache的优点。快排在进行递归部分的排序的时候,只会访问局部的数据,因此缓存能够更大概率的命中;而堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。简答的说就是快排的存取模型的局部性(locality)更强,堆排序差一些。速度和缓存的问题都反映了堆排序让数据过于大距离的移动,观察某个元素在整个排序过程中的移动过程,会发现它是前后大幅度的跑动;而快速排序则是尽快的移动到最终的位置,然后做小范围的跳动。
桶排序
假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。
在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。
public void bucketSort(int[] arr, int max){ if(arr==null || max<1){ return; } int[] buckets = new int[max]; // 计数 for(int i=0; i<arr.length; i++){ buckets[arr[i]]++; } // 排序 for(int j=0, k=0; j<max; j++){ while((buckets[j]--)>0){ arr[k++] = j; } } buckets = null; }
堆排序
a={20,30,90,40,70,110,60,10,100,50,80}, n=11 这是待排序的数组,对应的初始化结构为

分两步:1、调整成最大堆;2、交换数据排序
1、利用最大堆的向下调整算法,i=(n-1)/2, 从 i-- 处开始调整,到n=0时,得到的就是最大堆
(1)i=4。即调整70,较大的子树是80,交换。

(2)i=3。调整40,与100交换。

(3)i=2。调整90,与110交换。

(4)i=1.调整30,先交换100,再交换40.

(5)i=0. 调整20,先交换110,再交换90。

调整结束,得到最大堆。
2、排序。交换数据,把堆顶元素交换到堆末尾,然后向下调整,再次重复交换,再调整...
堆排序时间复杂度
堆排序的时间复杂度是O(N*lgN)。空间复杂度O(1).
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢?
堆排序是采用的二叉堆进行排序的,二叉堆就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。最多是多少呢?由于二叉堆是完全二叉树,因此,它的深度最多也不会超过lg(2N)。因此,遍历一趟的时间复杂度是O(N),而遍历次数介于lg(N+1)和lg(2N)之间;因此得出它的时间复杂度是O(N*lgN)。
堆排序是不稳定的算法.
代码:
public class HeapSort { /* * (最大)堆的向下调整算法 * * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 * 其中,N为数组下标索引值,如数组中第1个数对应的N为0。 * * a -- 待排序的数组 * start -- 被下调节点的起始位置 * end -- 截至范围 */ private static void maxHeapDown(int[] a, int start, int end) { int c = start; // 当前(current)节点的位置 int l = 2*c + 1; // 左(left)孩子的位置 int tmp = a[c]; // 当前(current)节点的大小 for (; l <= end; c=l, l=2*l+1) { // "l"是左孩子,"l+1"是右孩子 if ( l < end && a[l] < a[l+1]) l++; // 左右两孩子中选择较大者,即m_heap[l+1] if (tmp >= a[l]) break; // 调整结束 else { // 交换值 a[c] = a[l]; a[l]= tmp; } } } /* * 堆排序(从小到大) * * 参数说明: * a -- 待排序的数组 * n -- 数组的长度 */ public static void heapSortAsc(int[] a, int n) { int i, tmp; // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。 for (i = n / 2 - 1; i >= 0; i--) maxHeapDown(a, i, n-1); // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素 for (i = n - 1; i > 0; i--) { // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。 tmp = a[0]; a[0] = a[i]; a[i] = tmp; // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。 maxHeapDown(a, 0, i-1); } } }
冒泡排序
冒泡排序的时间复杂度是O(N2)
冒泡排序是稳定的算法
改进:添加一个标记,如果一趟遍历中发生了交换,则标记为true,否则为false。如果某一趟没有发生交换,说明排序已经完成!
public void bubbleSort1(int[] a){ int len = a.length; for(int i=len-1; i>0; i--){ for(int j=0; j<i; j++){ if(a[j]>a[j+1]){ int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } } // 改进 public void bubbleSort2(int[] a){ int len = a.length; boolean flag; for(int i=len-1; i>0; i--){ flag = false; for(int j=0; j<i; j++){ if(a[j] > a[j+1]){ int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; } } if(!flag){ break; } } }
插入排序
直接插入排序(Straight Insertion Sort)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。

直接插入排序的时间复杂度是O(N2)。
直接插入排序是稳定的算法。
public void insertSort(int[] arr) { int len = arr.length; int i, j, k; for(i=1; i<len; i++){ //为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置 for(j=i-1; j>=0; j--){ if(arr[j] < arr[i]){ break; } } //如找到了一个合适的位置 if(j!=i-1){ // j==i-1时不需要插入 int tmp = arr[i]; for(k=i-1; k>j; k--){ arr[k+1] = arr[k]; //将比a[i]大的数据向后移 } arr[k+1] = tmp; //将a[i]放到正确位置上 } } }
希尔排序
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
public void shellSort(int[] a){ int len = a.length; // gap为步长,每次减为原来的一半。 for(int gap=len/2; gap>0; gap /= 2){ // 共gap个组,对每一组都执行直接插入排序 for(int i=0; i<gap; i++){ for(int j=i+gap; j<len; j+=gap){ // 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。 if(a[j] < a[j-gap]){ int tmp = a[j]; int k=j-gap; while(k>=0 && a[k]>tmp){ a[k+gap] = a[k]; k -= gap; } a[k + gap] = tmp; } } } } }
选择排序
基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的时间复杂度是O(N2)。
选择排序是稳定的算法。
public void selectSort(int[] a){ int len = a.length; int i; // 有序区的末尾位置 int j; // 无序区的起始位置 int min; // 无序区中最小元素位置 for(i=0; i<len; i++){ min = i; for(j=i+1; j<len; j++){ if(a[j]<a[min]){ min=j; } } if(min != i){ int tmp = a[i]; a[i] = a[min]; a[min] = tmp; } } }
快速排序流程:
(1) 从数列中挑出一个基准值。
(2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
(3) 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。

快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
快速排序是不稳定的算法。
public static void quickSort(int[] arr, int left, int right){ if(left<right){ int i, j, x; i = left; j = right; x = arr[i]; // while(i<j){ while(i<j && arr[j]>x){ // j--; // 从右向左找第一个小于x的数 } if(i<j){ arr[i++] = arr[j]; } while(i<j && arr[i]<x){ i++; // 从左向右找第一个大于x的数 } if(i<j){ arr[j--] = arr[i]; } } arr[i] = x; quickSort(arr, left, i-1); quickSort(arr, i+1, right); } }
public class Quick2{ private int partition(int[] a, int left, int right){ int pivot = a[left]; while(left<right){ while(left<right && a[right]>pivot){ right--; } if(left<right){ a[left++] = a[right]; } while(left<right && a[left]<pivot){ left++; } if(left<right){ a[right--] = a[left]; } } a[left] = pivot; return left; } public void quickSort(int[] a, int left, int right){ if(left>=right){ return; } int index = partition(a, left, right); quickSort(a, left, index-1); quickSort(a, index+1, right); } }
归并排序
1. 从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。
2. 从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
② 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。
通过"从上往下的归并排序"来对数组{80,30,60,40,20,10,50,70}进行排序时:
1. 将数组{80,30,60,40,20,10,50,70}看作由两个有序的子数组{80,30,60,40}和{20,10,50,70}组成。对两个有序子树组进行排序即可。
2. 将子数组{80,30,60,40}看作由两个有序的子数组{80,30}和{60,40}组成。
将子数组{20,10,50,70}看作由两个有序的子数组{20,10}和{50,70}组成。
3. 将子数组{80,30}看作由两个有序的子数组{80}和{30}组成。
将子数组{60,40}看作由两个有序的子数组{60}和{40}组成。
将子数组{20,10}看作由两个有序的子数组{20}和{10}组成。
将子数组{50,70}看作由两个有序的子数组{50}和{70}组成。
归并排序的时间复杂度是O(N*lgN)。
归并排序是稳定的算法。
从上往下:
// 将一个数组中的两个相邻有序区间合并成一个 public void merge(int[] a, int start, int mid, int end) { int[] tmp = new int[end - start + 1]; // tmp是汇总2个有序区的临时区域 int i = start; // 第1个有序区的索引 int j = mid + 1; // 第2个有序区的索引 int k = 0; // 临时区域的索引 while (i <= mid && j <= end) { if (a[i] <= a[j]) tmp[k++] = a[i++]; else tmp[k++] = a[j++]; } while (i <= mid) tmp[k++] = a[i++]; while (j <= end) tmp[k++] = a[j++]; // 将排序后的元素,全部都整合到数组a中。 for (i = 0; i < k; i++) a[start + i] = tmp[i]; tmp = null; } // 归并排序(从上往下) public void mergeSortUp2Down(int[] a, int start, int end) { if (a == null || start >= end) return; int mid = (end + start) / 2; mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid] mergeSortUp2Down(a, mid + 1, end); // 递归排序a[mid+1...end] // a[start...mid] 和 a[mid...end]是两个有序空间, // 将它们排序成一个有序空间a[start...end] merge(a, start, mid, end); }
从下往上:

浙公网安备 33010602011771号