目录
排序算法总结分析(一)——开篇
排序算法总结分析(二)——常见八大排序算法
排序算法总结分析(三)——吃货排序之烙饼排序
1 冒泡排序(BubbleSort)
又译为泡沫排序或气泡排序,冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。
冒泡排序算法的运作如下:
由于它的简洁,很多旧版本的关于程式设计或者数据结构的入门书籍都是介绍冒泡算法。冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n2)次交换,而插入排序只要最多O(n)交换。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。
void bubble_sort(int array[],int len) { int i,j,temp; for(i=0;i<len;i++) { for(j=0;j<len-i-1;j++) { //交换 if(array[j]>array[j+1]) { temp=array[j]; array[j]=array[j+1]; array[j+1]=temp; } } } }
2选择排序(SelectSort)
选择排序是一种简单的交换排序算法,效率是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。冒泡排序与选择排序的比较次数是一样的,而移动次数比选择排序多出n次。
void selection_sort(int array[], int len) { int i, j, min, temp; for(i = 0; i < len - 1; i ++) { min = i; //查找最小值 for(j = i + 1; j < len; j ++) if(array[min] > array[j]) min = j; //交换 if(min != i) { temp = array[min]; array[min] = array[i]; array[i] = temp; } } }
3 插入排序(InsertSort)
插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。平均情况下,冒泡排序的比较次数近似为插入排序的2倍,移动次数与插入排序相同,可以说插入排序比冒泡排序快2倍。
插入排序一般不用在数据大于1000的场合,或者重复排序超过200数据项的序列。
具体算法描述如下:
void insertion_sort(int array[], int first, int last) { int i,j,temp; for (i = first+1; i<=last;i++) { temp = array[i]; j=i-1; //与已排序的数逐一比较,大于temp時,该数向后移动 while((j>=first) && (array[j] > temp)) { array[j+1] = array[j]; j--; } array[j+1] = temp; //被排序数放到正确的位置 } }
4 堆排序(HeapSort)
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序适合于数据量非常大的场合(百万数据)。堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。
堆排序的过程是:
void sift(int d[], int ind, int len) { //置i为要筛选的节点 int i = ind; //c中保存i节点的左孩子 int c = i * 2 + 1; //+1的目的就是为了解决节点从0开始而他的左孩子一直为0的问题 while(c < len)//未筛选到叶子节点 { //如果要筛选的节点既有左孩子又有右孩子并且左孩子值小于右孩子 //从二者中选出较大的并记录#% if(c + 1 < len && d[c] < d[c + 1]) c++; //如果要筛选的节点中的值大于左右孩子的较大者则退出 if(d[i] > d[c]) break; else { //交换 int t = d[c]; d[c] = d[i]; d[i] = t; // //重置要筛选的节点和要筛选的左孩子 i = c; c = 2 * i + 1; } } return; } void heap_sort(int d[], int n) { //初始化建堆, i从最后一个非叶子节点开始 for(int i = (n - 2) / 2; i >= 0; i--) sift(d, i, n); for(int j = 0; j < n; j++) { //交换 int t = d[0]; d[0] = d[n - j - 1]; d[n - j - 1] = t; //筛选编号为0 sift(d, 0, n - j - 1); } }
5 归并排序(MergeSort)
是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。
合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
归并操作的过程如下:
void merge_sort(int *list, int list_size) { if (list_size > 1) { // 把数组平均分成两个部分 int *list1 = list; int list1_size = list_size / 2; int *list2 = list + list_size / 2; int list2_size = list_size - list1_size; // 分别归并排序 merge_sort(list1, list1_size); merge_sort(list2, list2_size); // 归并 merge_array(list1, list1_size, list2, list2_size); } } void merge_array(int *list1, int list1_size, int *list2, int list2_size) { int i, j, k; i = j = k = 0; // 声明临时数组用于存储归并结果 int list[list1_size + list2_size]; // note: 只要有一个数组到达了尾部就要跳出 // 也就是说只有两个都没有到达尾部的时候才执行这个循环 while (i < list1_size && j < list2_size) { // 把较小的那个数据放到结果数组里, 同时移动指针 list[k++] = list1[i] < list2[j] ? list1[i++] : list2[j++]; } // 如果 list1 还有元素,把剩下的数据直接放到结果数组 while (i < list1_size) { list[k++] = list1[i++]; } // 如果 list2 还有元素,把剩下的数据直接放到结果数组 while (j < list2_size) { list[k++] = list2[j++]; } // 把结果数组 copy 到 list1 里 for (int ii = 0; ii < (list1_size + list2_size); ++ii) { list1[ii] = list[ii]; } }
6 快速排序(QuickSort)
在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。但对于内存非常有限的机器来说,它不是一个好的选择。
快速排序可以由下面四步组成。
1. 如果不多于1个数据,直接返回。
2. 一般选择序列最左边的值作为支点数据。
3. 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
4. 对两边利用递归排序数列。
void quick_sort(int data[], size_t left,size_t right) { size_t p = (left + right) / 2; int pivot = data[p]; size_t i = left,j = right; for ( ; i < j;) { while (! (i>= p || pivot < data[i])) ++i; if (i < p) { data[p] = data[i]; p = i; } while (! (j <= p || data[j] < pivot)) --j; if (j > p) { data[p] = data[j]; p = j; } } data[p] = pivot; if (p - left > 1) quick_sort(data, left, p - 1); if (right - p > 1) quick_sort(data, p + 1, right); }
7 Shell排序(ShellSort)
Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。也有说时间复杂度为O(n1.25)~O(1.6n1.25),查了下貌似没有具体解决。只有那么几种说法。一般说来,Shell排序比冒泡排序快5倍,比插入排序大致快2倍。它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
void shell_sort(int *data, size_t size) { for (int gap = size / 2; gap > 0; gap /= 2) for (int i = gap; i < size; ++i) { int key = data[i]; int j = 0; for( j = i -gap; j >= 0 && data[j] > key; j -=gap) { data[j+gap] = data[j]; } data[j+gap] = key; } }
8 基数排序(RadixSort)
基数排序和通常的排序算法并不走同样的路线。基数排序的时间复杂度是 O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),因为k的大小一般会受到 n 的影响。 以排序n个不同整数来举例,假定这些整数以B为底,这样每位数都有B个不同的数字,k就一定不小于logB(n)。由于有B个不同的数字,所以就需要B个不同的桶,在每一轮比较的时候都需要平均n·log2(B)次比较来把整数放到合适的桶中去,所以就有:
所以,基数排序的平均时间T就是:
T ≥logB(n)·n·log2(B) = log2(n)·logB(2)·n·log2(B)= log2(n)·n·logB(2)·log2(B) = n·log2(n)
所以和比较排序相似,基数排序需要的比较次数:T ≥ n·log2(n)。故其时间复杂度为 Ω(n·log2(n))= Ω(n·log n) 。
呃,这个没找到动图~~~~
int maxbit(int data[],int n) //辅助函数,求数据的最大位数 { int d = 1; //保存最大的位数 int p =10; for(int i = 0;i < n; ++i) { while(data[i] >= p) { p *= 10; ++d; } } return d; } void radixsort(int data[],int n) //基数排序 { int d = maxbit(data,n); int * tmp = new int[n]; int * count = new int[10]; //计数器 int i,j,k; int radix = 1; for(i = 1; i<= d;i++) //进行d次排序 { for(j = 0;j < 10;j++) count[j] = 0; //每次分配前清空计数器 for(j = 0;j < n; j++) { k = (data[j]/radix)%10; //统计每个桶中的记录数 count[k]++; } for(j = 1;j < 10;j++) count[j] = count[j-1] + count[j]; //将tmp中的位置依次分配给每个桶 for(j = n-1;j >= 0;j--) //将所有桶中记录依次收集到tmp中 { k = (data[j]/radix)%10; tmp[count[k]-1] = data[j]; count[k]--; } for(j = 0;j < n;j++) //将临时数组的内容复制到data中 data[j] = tmp[j]; radix = radix*10; } delete [] tmp; delete [] count; }
9 总结