常见排序算法
索引
1. 插入排序
1.1 直接插入
1.2 折半插入
1.3 希尔排序
2. 交换排序
2.1 冒泡排序
2.2 快速排序
3. 选择排序
3.1 直接选择
3.2 堆排序
4. 归并排序
4.1 迭代归并
总结
1. 插入排序
思想:每步将一个待排序的对象, 按其排序码大小, 插入到前面已经排好序的一组对象的适当位置上, 直到对象全部插入为止。
1.1 直接插入
1.1.1 方法:
当插入第i (i >= 1) 个对象时, 前面的V[0], V[1], …, V[i-1]已经排好序。这时, 用V[i]的排序码依次与V[i-1], V[i-2], …的排序码顺序进行比较, 找到插入位置即将V[i]插入, 原来位置上的对象向后顺移。
具体过程:
1. 把n个待排序的元素看成为一个“有序表”和一个“无序表”;
2. 开始时“有序表”中只包含1个元素,“无序表”中包含有n-1个元素;
3. 排序过程中每次从“无序表”中取出第一个元素,依次与“有序表”元素的关键字进行比较,将该元素插入到“有序表”中的适当位置,有序表个数增加1,直到“有序表”包括所有元素。
1.1.2 实例图:
1.1.3 代码:
/** * 直接插入排序:将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 /** * 直接插入排序 */ void direct_insert_sort(Type *array, int length) { Index i; Index j;//插入的位置 Type temp; for(i=1; i<length; i++) {//i=1,即从第二个元素开始(第一个元素下标为0) temp = array[i]; j=i; while(j>0 && array[j-1]>temp) { array[j] = array[j-1]; j--; } //如果i!=j,说明要插入到前面的“已续表”中 if(i!=j){ array[j] = temp; } } } int main(int argc, char **argv) { Type array[19] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};//待排序的数组 shell_sort(array, 19); //排序后,输出数组 for(int i=0; i<19; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
1.1.4 分析:
时间复杂度:O(n^2); 稳定的。
在下面两种情况,直接插入排序的效率较高:①序列中元素很少;②序列中的元素已经基本有序。
1.2 折半插入
1.2.1 方法:
在“直接插入排序”中,将在“有序表”中查找符合要求的项,利用二分查找完成。
1.2.2 实例图:
和“直接插入排序”排序过程相同。(两者只是“查找插入位置的方法”不同)
1.2.3 代码:
/** * 折半插入排序 */ void binary_insert_sort(Type *array, int length) { Index i; Index k; Index left, right;//二分查找时记录左右两侧的下标 Type temp; for(i=1; i<length; i++) { left = 0; right = i-1;//查找时,不包括第i个,因为是要将第i个插入到合适的位置 temp = array[i]; while(left<=right) { Index middle = (left+right)/2; if( array[middle]<=temp ){ //注意:当middle==temp时,要是left加1。否则,算法将“不稳定” left = middle+1; }else{ right = middle-1; } } for(k=i; k>left; k--){ array[k] = array[k-1]; } array[left] = temp; } } int main(int argc, char **argv) { Type array[19] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};//待排序的数组 shell_sort(array, 19); //排序后,输出数组 for(int i=0; i<19; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
1.2.4 分析:
时间复杂度:O(N*logN); 稳定的(注意若处于后面“无序表”中的项,若等于前面“有序表”中的项,要将该项插入到“有序表”中对应项的后面)。
相对于“直接插入排序”:比较次数比“直接插入排序”的最差情况要好得多,但是比“直接插入排序”的最好情况要差,尤其是当数组已经排好序或者接近有序的时候。也就是说,“折半插入排序”不是在所有情况都优于“直接插入排序”。
1.3 希尔排序(缩小增量排序)
1.3.1 方法:
因为在“直接插入排序”过程中,若元素已经基本有序,那么“直接插入排序”的效率较高。引出了“希尔排序”的基本思想:
1. 设待排序的序列有 n 个对象,首先取一个整数 gap < n 作为间隔, 将下标相差为gap的倍数对象放在一组。
2. 在组内作 直接插入排序。
3. 然后逐渐缩小间隔 gap, 例如取 gap = gap/2,重复上述的组划分和排序工作。直到最后取 gap == 1, 将所有对象放在同一个组中进行排序为止。
1.3.2 实例图:
1.3.3 代码:
/** * 希尔排序:将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 /** * 希尔排序 */ void shell_sort(Type *array, int length) { Index i; Index j;//插入的位置 Type temp; int gap = length/2;//子序列间隔,这里取长度的一半 while(gap!=0) { for(i=gap; i<length; i+=gap) { //1. i从gap开始取值;2. i每次递增gap temp = array[i]; j=i; while(j>=gap && array[j-gap]>temp) { array[j] = array[j-gap]; j -= gap; } //如果i!=j,说明要插入到前面的“已续表”中 if(i!=j) { array[j] = temp; } } gap /= 2; } } int main(int argc, char **argv) { Type array[19] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};//待排序的数组 shell_sort(array, 19); //排序后,输出数组 for(int i=0; i<19; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
1.3.4 分析:
时间复杂度:n^1.25 ~ 1.6*n^1.25之间(统计资料),是不稳定的;gap的取值会影响希尔排序的效率。
2. 交换排序
思想:两两比较待排序对象的排序码,如果发生逆序,则进行交换。直到所有对象都排好序为止。
2.1 冒泡排序
2.1.1 方法:
1. 对待排序序列从前向后(从下标较大的元素开始)依次比较相邻元素的关键字,若发现逆序则交换;
2. 使较小的元素逐渐前移(或者较大的元素逐渐后移);(假定按照“从小到大”排序)
改进措施:
如果一趟比较下来没有进行过交换,就说明序列已经有序;可以通过设置一个标志exchange记录一趟遍历中是否进行了交换。
2.1.2 实例图:
2.1.3 代码:
/** * 冒泡排序:将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 /* * 方法一:每次将最小的元素推至前端 */ void bubble_sort1(Type *array, int length) { Index i, j; bool exchange;//标志一次遍历中,是否进行了交换 for(i=0; i<length-1; i++) { exchange = false;//每次循环开始时,值为false for(j=length-1; j>i; j--) { if(array[j]<array[j-1]) { //两两比较,当两者相等时,不交换,这样才能使该排序稳定,即原来在前面的排序后也在前面 exchange = true; Type temp = array[j-1]; array[j-1] = array[j]; array[j] = temp; } } //如果本次循环没有交换,说明数组已经排序完毕 if(!exchange) { break; } } } /* * 方法二:每次将最大的元素推至末尾 */ void bubble_sort2(Type *array, int length) { Index i, j; bool exchange;//标志一次遍历中,是否进行了交换 for(i=0; i<length-1; i++) { exchange = false;//每次循环开始时,值为false for(j=0; j<length-i-1; j++) { if(array[j]>array[j+1]) { //两两比较,和“将较小元素推至前端”相同,当两者相等时,不交换,这样才能使该排序稳定,即原来在后面的排序后也在后面 exchange = true; Type temp = array[j+1]; array[j+1] = array[j]; array[j] = temp; } } //如果本次循环没有交换,说明数组已经排序完毕 if(!exchange) { break; } } } int main(int argc, char **argv) { Type array[19] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};//待排序的数组 bubble_sort1(array, 19); //排序后,输出数组 for(int i=0; i<19; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
2.1.4 分析:
时间复杂度:O(n^2); 稳定的。
2.2 快速排序
2.2.1 方法:
“冒泡排序”中,元素的比较是从一端到另一端进行,每次移动一个位置;如果要是能“从两端到中间”进行,比“基准元素”大的一次就能交换到后面单元,比“基准元素”小的一次就能交换到前面单元,每次移动的距离较远,从而总的比较次数和移动次数都会减少。
步骤:(利用分治的算法思想)
1. 任取待排序序列中的某个元素作为基准(一般取第一个元素);
2. 通过一趟排序,将待排元素分为左右两个子序列:
左子序列元素的关键字均小于或等于基准元素的关键字;
右子序列的关键字则大于基准元素的关键字;
3. 分别对两个子序列继续进行排序,重复以上步骤,直至整个序列有序。(是一个递归的过程)
2.2.2 实例图:
当 i 为1时,执行过程为:
2.2.3 代码:
/** * 快速排序:将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 void quick_sort_recursion(Type *array, Index left, Index right); int partition(Type *array, Index left, Index right); /** * 快速排序,接口,内部调用“递归实现函数”quick_sort_recursion */ void quick_sort(Type *array, int length) { quick_sort_recursion(array, 0, length-1); } /** * 快速排序,递归实现函数 */ void quick_sort_recursion(Type *array, Index left, Index right) { if(left<right) { int pivot_index = partition(array, left, right);//获得基准位置 quick_sort_recursion(array, left, pivot_index-1);//递归调用,将子序列排序,注意不包括pivot_index位置的元素 quick_sort_recursion(array, pivot_index+1, right); } /* 调用这里的left和right都是有效的下标。如果类似C++ STL中,right是末尾的下一位,应写为下面的形式: 原则:①调用partition函数的参数都是有效的;②调用自身quick_sort_recursion的参数right是末尾的下一个; if(left<right) { int pivot_index = partition(array, left, right-1);//使用right-1 quick_sort_recursion(array, left, pivot_index);//使用pivot_index quick_sort_recursion(array, pivot_index+1, right); } 相应的,在quick_sort函数中的调用形式应修改为:quick_sort_recursion(array, 0, length); */ } /** * 利用第一元素作为基准元素,将整个序列划分为两个部分。 * 步骤: * 1. 比基准元素小的都移动到左侧,比基准元素大的都移动到右侧,变量pivot_position始终记录着比基准元素小的元素的最后一个元素。 * 2. 最后将基准元素(第一个)与pivot_position位置上的元素交换,就可以达到目的。 * 这时基准元素所在的位置也就是最终排序完成后,应该在的位置 * 注意:这个实现中,参数中的left和right都是有效的,特别注意的是,这里的right是序列最后一个元素的下标,不是最后一个元素的下一个。 */ int partition(Type *array, Index left, Index right) { Type pivot = array[left];//基准元素值 Index pivot_index = left; //记录已比较的元素中,比基准元素小的元素的最后一个位置;也是最后要返回的位置 Index i; for(i=left+1; i<=right; i++) {//这里 i 能够等于right,就要求传给partition的参数中的right参数,一定要是有效的 if(array[i]<pivot) { pivot_index++; //交换array[pivot_index]和array[i] if(i!=pivot_index) { Type temp = array[i]; array[i] = array[pivot_index]; array[pivot_index] = temp; } } } //交换基准元素(第一个元素)和array[pivot_index] array[left] = array[pivot_index]; array[pivot_index] = pivot; return pivot_index; } #define ARRAY_LENGTH 18 int main(int argc, char **argv) { Type array[ARRAY_LENGTH] = { 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0};//待排序的数组 //调用接口函数 quick_sort(array, ARRAY_LENGTH); //排序后,输出数组 for(int i=0; i<ARRAY_LENGTH; i++) { cout<<array[i]<<" "; } cout<<endl; //调用递归实现函数 quick_sort_recursion(array, 0,ARRAY_LENGTH); //排序后,输出数组 for(int i=0; i<ARRAY_LENGTH; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
2.2.4 分析:
快速排序是一个“递归算法”,快速排序的趟数等于递归树的高度。 时间复杂度:最好 O(N*logN);最差 O(N^2);
空间复杂度:最好 O(logN); 最差 O(N);
是一种不稳定的算法。但是当序列元素数量较多时,快速排序的效率一般很好,所以经常采用;但是当元素较少时,其比一般方法可能要慢(因为要递归)。
3. 选择排序
思想:每一趟遍历,都从序列中找到最小的元素,将其放到队列的开始位置。或者找到序列的最大元素,放到队列的末尾。
3.1 直接选择排序
3.1.1 方法:
1. 在一组对象 V[i]~V[n-1] 中选择最小的对象;
2. 将它与这组对象中的第一个对象对调;
3. 在剩下的对象V[i+1]~V[n-1]中重复执行第①、②步, 直到剩余对象只有一个为止。
3.1.2 实例图:
/** * 直接选择排序:将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 /** * 直接选择排序,每次选取最小的替换到开头 */ void direct_select_sort(Type *array, int length) { Index i, j; Index min_index;//每趟循环中,最小元素的下标 Type temp; for(i=0; i<length-1; i++) {// i 的取值范围是从0到length-2,不包括最后一个元素(下标为 length-1),因为只剩一个元素时不需要判断 min_index = i; for(j=i+1; j<length; j++) {//从i+1开始 if(array[j]<array[min_index]) { min_index = j; } } temp = array[i]; array[i] = array[min_index]; array[min_index] = temp; } } #define ARRAY_LENGTH 18 int main(int argc, char **argv) { Type array[ARRAY_LENGTH] = { 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0};//待排序的数组 direct_select_sort(array, ARRAY_LENGTH); //排序后,输出数组 for(int i=0; i<ARRAY_LENGTH; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
3.1.4 分析:
时间复杂度:O(n^2)。不稳定。
总的比较次数固定为:n*(n-1)/2次,即O(n^2)级别。
3.2 堆排序
3.2.1 方法:
1. 根据初始输入数据,利用堆的“下滤调整算法”形成初始最大堆;
2. 将最大元素换到最后一个元素,对前面的元素构建最大堆。
3. 重复执行以上两步,直到所有元素排序完成。
3.2.2 实例图:
3.2.3 代码:
/** * 堆排序:将数组从小到大排序 * 方法:依次建立最大堆,将堆顶元素(最大元素)与最后一个元素交换; */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 void filter_down(Type *heap, Index pos, int length); void build_heap(Type *array, int length); void heap_sort(Type *array, int length); /** * 堆的下滤操作 */ void filter_down(Type *heap, Index pos, int length) { Index current=pos;//记录下滤过程中的当前节点 Index child = 2*pos+1;//当前节点的子节点,当下标从0开始时,找到左孩子的下标 Type temp = heap[pos]; while(child<length) { //若左右孩子都存在,找到左右孩子中最大的那个 if(child+1<length && heap[child]<heap[child+1]) { ++child; } //如果当前节点小于 其 孩子,将“子节点的值”赋给“当前节点” if(temp<heap[child]) { heap[current] = heap[child]; } else { break; } current = child; child = 2*child + 1; } heap[current] = temp; } /** * 建立 堆 */ void build_heap(Type *array, int length) { Index i; for(i=(length-2)/2; i>=0; i--) { filter_down(array, i, length); } } /** * 堆排序 */ void heap_sort(Type *array, int length) { //建立堆 build_heap(array, length); Index i; Type temp; for(i=length-1; i>0; i--) {// i 的范围从length-1 到 1,共length-2次循环(不包括i=0),若只剩一个元素,则说明整个序列已经有序了。 //交换 temp = array[0]; array[0] = array[i]; array[i] = temp; //下滤 filter_down(array, 0, i); } } #define ARRAY_LENGTH 18 int main(int argc, char **argv) { Type array[ARRAY_LENGTH] = { 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0};//待排序的数组 heap_sort(array, ARRAY_LENGTH); //排序后,输出数组 for(int i=0; i<ARRAY_LENGTH; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
3.2.4 分析:
时间复杂度:O(n*logn);
空间复杂度:O(1);
不稳定。
4. 归并排序
思想:将两个或两个以上的“有序表”合并成一个新的“有序表”。
4.1迭代归并
4.1.1 方法:
利用“两路归并”过程进行,步骤如下:
1. 把序列看成是 n 个长度为 1 的有序子序列 (归并项),先做两两归并,得到 n / 2 个长度为 2 的归并项 (如果 n 为奇数,则最后一个有序子序列的长度为1);
2. 然后看成长度为2, 4, 8……的有序子序列,两两归并,直到得到长度为n的有序序列;
注意:
如果n不是2len的整数倍, 则一趟归并到最后,可能遇到两种情形:
1. 剩下一个长度为len的归并项和另一个长度不足len的归并项, 可用merge算法将它们归并成一个长度小于 2len 的归并项;
2. 只剩下一个归并项,其长度小于或等于 len, 将它直接抄到目标序列中。
4.1.2 实例图:
4.1.3 代码:
/** * 归并排序(迭代实现):将数组从小到大排序 */ #include <iostream> using namespace std; typedef int Index;//下标的别名 typedef int Type;//待排序的数组的元素类型 void merge(Type *src, Type *dest, Index left, Index middle, Index right); void merge_pass(Type *src, Type *dest, int section, int length); void merge_sort(Type *array, int length); /** * 合并两个列表 * 将src中[left, middle]位置,和src中[middle+1, right]开始位置的元素合并到dest中 * 注意:上面的区间是闭区间 */ void merge(Type *src, Type *dest, Index left, Index middle, Index right) { Index i = left; //在src的[left, middle]中前进 Index j = middle+1; //在src的[middle+1, right]中前进 Index k = left; //在dest中前进 while(i<=middle && j<=right) { if(src[i]<=src[j]) { //注意:因为src[i]在前面,为了是算法稳定,当src[i]==src[j]时,应先添加stc[i] dest[k++] = src[i++]; } else { dest[k++] = src[j++]; } } //[left, middle]还没有走完 while(i<=middle) { dest[k++] = src[i++]; } //[middle+1, right]还没有走完 while(j<=right) { dest[k++] = src[j++]; } } /** * 归并排序的具体执行函数 */ void merge_pass(Type *src, Type *dest, int section, int length) { Index i=0; //1. 合并开始部分 while(i+2*section <= length) {//因为序列下标从0开始,有等号 merge(src, dest, i, i+section-1, i+2*section-1); i += 2*section; } //2.剩余部分有两块 if(i+section<length) { merge(src, dest, i, i+section-1, length-1); } else { //3. 只剩一部分,直接添加到末尾 while(i<length) {//这里没有等号 dest[i] = src[i]; i++; } } } /** * 归并排序 */ void merge_sort(Type *array, int length) { int section = 1;//小部分的长度 Type *help_array = new Type[length];//辅助数组 while(section<length) { //section不需要等于length //从array归并到help_array merge_pass(array, help_array, section, length); section *= 2; //从help_array归并到array merge_pass(help_array, array, section, length); section *= 2; } delete []help_array; } #define ARRAY_LENGTH 18 int main(int argc, char **argv) { Type array[ARRAY_LENGTH] = { 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0};//待排序的数组 merge_sort(array, ARRAY_LENGTH); //排序后,输出数组 for(int i=0; i<ARRAY_LENGTH; i++) { cout<<array[i]<<" "; } cout<<endl; return 0; }
4.1.4 分析:
时间复杂度:O(N*logN);
空间复杂度:一个和原来数组一样大小的数组,O(N);
该算法是稳定的。
5. 总结
排 序 方 法 | 比较次数 | 移动次数 | 稳定性 | 附加存储 | |||
最好 | 最差 | 最好 | 最差 | 最好 | 最差 | ||
直接插入排序 | n | n^2 | 0 | n^2 | Ö | 1 | |
折半插入排序 | n logn | 0 | n^2 | Ö | 1 | ||
起泡排序 | n | n^2 | 0 | n^2 | Ö | 1 | |
快速排序 | nlogn | n^2 | < nlogn | n^2 | ´ | logn | n |
直接选择排序 | n^2 | 0 | n | ´ | 1 | ||
堆排序 | n logn | n logn | ´ | 1 | |||
归并排序 | n logn | n logn | Ö | n |