各种排序算法
2015-09-27 19:01
344 查看
冒泡排序
思路:前后两两比较,如果前>后,则交换。每次从后向前扫描,所以每次扫描后最小的数就已经被交换到最前面了,需要扫描length次。
时间复杂读O(n2)。
稳定排序。稳定性,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
选择排序
思路:每次扫描序列,找到最小的,放到头部
时间复杂度O(n2)
因为每次循环找出的最小数会与第一个元素交换位置,所以元素顺序被打乱了,非稳定排序。
插入排序
思路:把元素插入到已排好序列的合适的位置,每插入一个元素,排好的序列就增长1。
时间复杂读O(n2)。
稳定排序。
希尔排序
思路:是对插入算法的改进,减少了元素交换的次数,要被插入的元素不是一个一个往前移动判断,而是以dt的步长向前移动。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成(n除以d1)个组。所有除以d1余数相等的记录放在同一个组中。先在各组内进行直接插入 排序;然后,取第二个增量d2重复上述的分组和排序,直至所取的增量dt=1,即所有记录放在同一组中进行直接插入排序为止。一般的初次取序列的一半为增 量,以后每次减半,直到增量为1。
好处:可以让元素一次性地朝最终位置迈进一大步,然后再取小步长进行小碎步调整。尤适用于最小的元素恰巧在末尾,那么使用插入排序需要一步一步向前移动和比较。
时间复杂度分析:O(nlogn) - O(n2) ,与增量的选取算法有关
不稳定排序。
快速排序
思路:选取一个中间值,大于他的放在他的右边,小于他的则放在左边,然后对左边序列及右边序列分别再进行递归。
时间复杂度分析:希尔排序,快速排序,堆排序都是采用了分治法,最外层循环至少进行logn次。每次循环要进行n/(2^i)次比较和3次移动,时间复杂度在平均情况和最好情况下是O(nlogn),在最坏情况下是O(n2)。
不稳定排序。
改进:
1.本算法中是将start位置的元素作为中间值,改进方法是选择接近平均值的元素作为中间值,比如取出三个元素取平均作为中间值
2.本算法通过前后两个指针扫描,前者扫描到>=目标值的元素停止,后者扫描到<=目标值的元素停止,交换指针所指的两个值,继续扫描,直到 前指针位置大于后指针。这样做虽然增加了交换的次数,但在元素全相等的情况下,时间复杂度还是O(nlogn);而如果只是用一个mid指针,在元素全相 等的情况下,需要n次调用递归,时间复杂度O(n2)。
归并排序
思路:把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列两两合并为整体有序序列。
不同于快速排序(先排序再二分),归并排序是先二分,再将二分法的结果进行排序
稳定排序。由于在合并子序列的过程中使用的是插入排序,所以归并排序也是稳定排序。
改进:递归以及并行操作的优化都是,设置一个阈值,比如7,对于<7的子序列,不再递归,而是采用直接插入排序。
递归问题时间复杂度的计算方法:
法I:看成一颗二叉树,有lgN+1层,每层有N个节点。所以时间复杂度O(n)=(lgN+1)N=NlgN+N=NlgN
法II:递归表达式求解的一种常用方法是数学归纳法。
首先猜测O(N)=NlgN
当N=1时,O(1)=0,满足初始条件。
假设O(2k)=2klg2k=k*2k
那么O(2k+1)=(2k+1)(lg2k+1)=2klg2k+lg2k+2k+1=k*2k+k+2k+1=2k(k+1)+(k+1)=(k+1)*(2k+1)
所以假设成立,O(N)=NlgN
堆排序
思路:最大堆是最大的一个数在堆顶,父节点总比子节点大(新添节点放在叶子节点,自下而上调整)。然后我们将堆顶放到序列的尾部,选一个叶子节点放到堆顶,利用循环自上而下调整最大堆。
堆排序的建堆时间复杂度是O(n),证明:易知时间复杂度的上限是O(nlogn),但是每个节点的调整高度不是单纯的LogN。对于高度为1的节点(叶子节点的高度为0) ,至多替换发生1次。对于高度为2的节点,至多替换发生2次,以此类推,对于高度为h的节点,至多发生替换h次。我们知道,堆是满树,叶节点共有N/2个,它们的高度是0 。高度为1的节点正是他们的父节点,共有(N/2)/2=N/22个。高度为2的,类推有((N/2)/2)/2=N/23个。因此高度为h的共有=N/2h+1个。总的交换次数为
所以建堆的时间复杂度是O(n)。排序的时间复杂度最差情况下是O(nlogn)。
堆排序的优势:对排序最坏时间复杂度是O(nlogn),而快速排序最坏时间复杂度是O(n2);堆排序无需额外的空间,而归并排序需要O(n)
堆排序的劣势:其内部循环要比快速排序长;操作在N和N/2之间进行比较和交换,当数组较长时,对CPU缓存利用效率比较低;非稳定排序
思路:前后两两比较,如果前>后,则交换。每次从后向前扫描,所以每次扫描后最小的数就已经被交换到最前面了,需要扫描length次。
时间复杂读O(n2)。
稳定排序。稳定性,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
public class BubbleSort implements SortUtil.Sort{ public void sort(int[] data){ for (int i = 0; i < data.length; i++ ){//遍历待排序的位 for (int j = data.length-1; j>i ; j--){ //交换到最后,第一位必定是最小的 if (data[j] < data[j-1]) SortUtil.swap(data, j, j-1); } } } }
选择排序
思路:每次扫描序列,找到最小的,放到头部
时间复杂度O(n2)
因为每次循环找出的最小数会与第一个元素交换位置,所以元素顺序被打乱了,非稳定排序。
public class SelectSort implements SortUtil.Sort{ public void sort(int[] data){ int temp; for (int i = 0; i < data.length; i++ ){ temp = i; for (int j = data.length-1; j>i ; j--){ if (data[j] < data[temp]) temp = j; } SortUtil.swap(data, i, temp); } } }
插入排序
思路:把元素插入到已排好序列的合适的位置,每插入一个元素,排好的序列就增长1。
时间复杂读O(n2)。
稳定排序。
public class InsertSort implements SortUtil.Sort{ public void sort(int[] data){ for (int i = 1; i < data.length; i++ ) { //遍历待排序的位 for (int j = i ; j>0 && data[j-1]> data[j]; j--){ //从后往前(数组,从最后一位开始移位操作) SortUtil.swap(data, j, j-1); } } } }
希尔排序
思路:是对插入算法的改进,减少了元素交换的次数,要被插入的元素不是一个一个往前移动判断,而是以dt的步长向前移动。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成(n除以d1)个组。所有除以d1余数相等的记录放在同一个组中。先在各组内进行直接插入 排序;然后,取第二个增量d2重复上述的分组和排序,直至所取的增量dt=1,即所有记录放在同一组中进行直接插入排序为止。一般的初次取序列的一半为增 量,以后每次减半,直到增量为1。
好处:可以让元素一次性地朝最终位置迈进一大步,然后再取小步长进行小碎步调整。尤适用于最小的元素恰巧在末尾,那么使用插入排序需要一步一步向前移动和比较。
时间复杂度分析:O(nlogn) - O(n2) ,与增量的选取算法有关
不稳定排序。
void insertSort(int* int_array,int start, const int len, int inc){ //对步长为inc的子序列进行排序 int temp; for(int i = start; i < len; i+=inc){ //for循环的增量为inc for(int j = i; j>0; j-= inc){ if(int_array[j] < int_array[j-inc]){ temp = int_array[j]; int_array[j] = int_array[j-inc]; int_array[j-inc] = temp; } } } } void shellSort(int* int_array, const int len){ for(int inc = len/2; inc >0; inc/=2) {//先对inc由大到小遍历 for(int i = 0; i < inc; i++) {//遍历组 insertSort(int_array, i, len, inc);//在每组内进行插入排序 } } }
快速排序
思路:选取一个中间值,大于他的放在他的右边,小于他的则放在左边,然后对左边序列及右边序列分别再进行递归。
时间复杂度分析:希尔排序,快速排序,堆排序都是采用了分治法,最外层循环至少进行logn次。每次循环要进行n/(2^i)次比较和3次移动,时间复杂度在平均情况和最好情况下是O(nlogn),在最坏情况下是O(n2)。
不稳定排序。
void sort(int* int_array, int start, int end) { if(start == end ) return; int p1=start+1; int p2 = end; int tmp; //range the array into two part according to the sentinel value while(1){ while(int_array[p1] <= int_array[start]) p1++; while(int_array[p2] > int_array[start]) p2--; if(p1>=p2) break; tmp = int_array[p1]; int_array[p1] = int_array[p2]; int_array[p2] = tmp; } //put the sentinel into the middle of the array tmp = int_array[start]; int_array[start] = int_array[p2]; int_array[p2]=tmp; //recursion sort(int_array,start,p2); sort(int_array,p1,end); } void quickSort(int* int_array, int len) { sort(int_array,0,len-1); }
改进:
1.本算法中是将start位置的元素作为中间值,改进方法是选择接近平均值的元素作为中间值,比如取出三个元素取平均作为中间值
2.本算法通过前后两个指针扫描,前者扫描到>=目标值的元素停止,后者扫描到<=目标值的元素停止,交换指针所指的两个值,继续扫描,直到 前指针位置大于后指针。这样做虽然增加了交换的次数,但在元素全相等的情况下,时间复杂度还是O(nlogn);而如果只是用一个mid指针,在元素全相 等的情况下,需要n次调用递归,时间复杂度O(n2)。
归并排序
思路:把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列两两合并为整体有序序列。
不同于快速排序(先排序再二分),归并排序是先二分,再将二分法的结果进行排序
稳定排序。由于在合并子序列的过程中使用的是插入排序,所以归并排序也是稳定排序。
void sort(int* int_array, int* result_array, int start, int end) { if(start==end) return; int mid = (start + end)/2; sort(int_array,result_array,start,mid); sort(int_array,result_array,mid+1,end); //现在两个子序列是有序的,开始合并这两个子序列 int p1 = start; int p2 = mid+1; int rp = start; while(p1 <= mid && p2 <=end){ //要额外申请一个空间result_array保存数组。 if(int_array[p1] <= int_array[p2]) result_array[rp++] = int_array[p1++]; else result_array[rp++] = int_array[p2++]; } while(p1<=mid) result_array[rp++] = int_array[p1++]; while(p2<=end) result_array[rp++] = int_array[p2++]; for(int i = start; i<=end; i++) int_array[i] =result_array[i]; } void mergeSort(int* int_array, const int len) { int* result_array = new int[len]; sort(int_array,result_array,0,len-1); }
改进:递归以及并行操作的优化都是,设置一个阈值,比如7,对于<7的子序列,不再递归,而是采用直接插入排序。
递归问题时间复杂度的计算方法:
法I:看成一颗二叉树,有lgN+1层,每层有N个节点。所以时间复杂度O(n)=(lgN+1)N=NlgN+N=NlgN
法II:递归表达式求解的一种常用方法是数学归纳法。
首先猜测O(N)=NlgN
当N=1时,O(1)=0,满足初始条件。
假设O(2k)=2klg2k=k*2k
那么O(2k+1)=(2k+1)(lg2k+1)=2klg2k+lg2k+2k+1=k*2k+k+2k+1=2k(k+1)+(k+1)=(k+1)*(2k+1)
所以假设成立,O(N)=NlgN
堆排序
思路:最大堆是最大的一个数在堆顶,父节点总比子节点大(新添节点放在叶子节点,自下而上调整)。然后我们将堆顶放到序列的尾部,选一个叶子节点放到堆顶,利用循环自上而下调整最大堆。
class MaxHeap { //建堆 public: int * queue; MaxHeap::MaxHeap(int* data, const int len) { queue = new int (len+1);//queue[0]空着,以便实现子是父的两倍关系 for(int i = 1; i <= len; i++) { queue[i] = data[i-1]; fixUp(i); } } void fixUp(int k){ //k是队列下标 int j = k >> 1; //右移一位,相当于除以2,即找到父节点 int temp; while(j>0){ if(queue[j]>queue[k]) break; temp = queue[j]; queue[j] = queue[k]; queue[k] = temp; k = j; //向上移一层 j = k>>1; } } }; void fixDown(int* queue, const int queue_size, int k){ //排序 int j = k << 1; //找左儿子 int temp; while(j<=queue_size){ if (j!=queue_size && queue[j] < queue[j+1]) j++;//取左右儿子中大的那个 if(queue[j] > queue[k]){ temp = queue[j]; queue[j] = queue[k]; queue[k] = temp; k = j; j = k << 1; } } } void heapSort(int* int_array, const int len) { MaxHeap maxHeap(int_array, len); int queue_size = len; for(int i = len-1; i >= 0; i--) { int_array[i] = maxHeap.queue[1]; maxHeap.queue[1] = maxHeap.queue[queue_size--]; fixDown(maxHeap.queue,queue_size, 1); } }
堆排序的建堆时间复杂度是O(n),证明:易知时间复杂度的上限是O(nlogn),但是每个节点的调整高度不是单纯的LogN。对于高度为1的节点(叶子节点的高度为0) ,至多替换发生1次。对于高度为2的节点,至多替换发生2次,以此类推,对于高度为h的节点,至多发生替换h次。我们知道,堆是满树,叶节点共有N/2个,它们的高度是0 。高度为1的节点正是他们的父节点,共有(N/2)/2=N/22个。高度为2的,类推有((N/2)/2)/2=N/23个。因此高度为h的共有=N/2h+1个。总的交换次数为
所以建堆的时间复杂度是O(n)。排序的时间复杂度最差情况下是O(nlogn)。
堆排序的优势:对排序最坏时间复杂度是O(nlogn),而快速排序最坏时间复杂度是O(n2);堆排序无需额外的空间,而归并排序需要O(n)
堆排序的劣势:其内部循环要比快速排序长;操作在N和N/2之间进行比较和交换,当数组较长时,对CPU缓存利用效率比较低;非稳定排序
排序算法 | 最好时间 | 平均时间 | 最差时间 | 稳定否 | 空间复杂度 |
冒泡 | O(n) | O(n2) | O(n2) | 稳定 | O(1) |
选择 | O(n2) | O(n2) | O(n2) | 不稳定 | O(1) |
插入 | O(n) | O(n2) | O(n2) | 稳定 | O(1) |
Shell | O(nlogn) | O(nlogn) | O(n^s) 1 | 不稳定 | O(1) |
快速 | O(nlogn) | O(nlogn) | O(n2) | 不稳定 | O(logn) (递归栈上需要花费最少logn 最多n的空间) |
归并 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | O(n) |
堆 | O(nlogn) | O(nlogn) | O(nlogn) | 不稳定 | O(1) |