本文章包括全部基本排序算法(和其中一些算法的改进算法):
直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序、快速排序、归并排序、基数排序。html
一个插入排序是另外一种简单排序,它的思路是:每次从未排好的序列中选出第一个元素插入到已排好的序列中。
它的算法步骤能够大体概括以下:git
所以,从上面的描述中咱们能够发现,直接插入排序能够用两个循环完成:
第一层循环:遍历待比较的全部数组元素
第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
若是:selected > ordered,那么将两者交换算法
#include <stdio.h> void InsertSort(int *a, int len) { int i, j, tmp; for(i=1; i<len; i++){ //i指向无序区第一个元素 tmp = a[i]; j = i - 1; //j指向有序去第一个元素 // j往前遍历,找到比a[i]小的,插入到此处。比a[i]大的后移 while(j>=0 && tmp<a[j]){ //小于号换成大于号则是从大到小排序 a[j+1] = a[j]; j--; } a[j+1] = tmp; //插入到空出来的位置 } return; } int main() { int a[] = {1,3,63,5,78,9,12,52,8}; int n = sizeof(a)/sizeof(int),i; InsertionSort(a, n); for(i=0; i<n; i++) printf("%d ", a[i]); return 0; }
与直接插入算法的区别在于:在有序表中寻找待排序数据的正确位置时,使用了折半查找/二分查找。
减小了比较的次数,但没有减小插入的次数。时间复杂度仍为O(n^2),但比直接插入排序稍微快一点。shell
void BinaryInsertSort(int *a, int len) { int i, j, low, high, tmp; for(i=1; i<len; i++){ tmp = a[i]; j = i - 1; low = 0; high = i - 1; while(low<=high){ int mid = (low + high) / 2; if(tmp>a[mid]) low = mid + 1; else high = mid - 1 ; } while(j>=low && tmp<a[j]){ //小于号换成大于号则是从大到小排序 a[j+1] = a[j]; j--; } a[j+1] = tmp; } return; }
希尔排序又叫缩小增量排序数组
基于直接插入排序,基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。数据结构
希尔的思想也很简单就是一个h-sort的插入算法——每相邻h个元素进行插入排序
若是h比较大,那么子数组会很小,用插入效率高
若是h很小,这时候数组基本有序,插入效率也很高ide
void ShellSort(int arr[], int len) { int step; int i, j; int tmp; for(step = len/2; step > 0; step = step/2) // 比直接插入排序多了一层循环 { for(i = step; i < len; i++) // 直接插入排序能够当作step为1的希尔排序。把这里的Step都替换成1,就是直接插入排序 { j = i - step; tmp = arr[i]; while(j>=0 && tmp<arr[j]) { arr[j+step] = arr[j]; j = j - step; } arr[j+step] = tmp; } } return; }
排序思路是:每次从未排序的序列中选出一个最小值,并把它放在已排好序的序列的序尾。这样就造成了一个有序序列(从小到大)。
简单选择排序的基本思想:找到最小值 + 交换。函数
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
第二趟,从第二个记录开始的 n-1 个记录中再选出关键码最小的记录与第二个记录交换;
以此类推.....
第 i 趟,则从第 i 个记录开始的 n-i+1 个记录中选出关键码最小的记录与第 i 个记录交换,
直到整个序列按关键码有序。性能
void SelectSort(int *a, int len) { int i, j; int key, tmp; for(i=0; i<len; i++) { key = i; // 记录最小值的下标,初始值为i,即有序序列的最后一个元素 for(j=i+1; j<len; j++) // j从i的下一个元素开始遍历,即无序序列的第一个元素 { if(a[j]<a[key]) // 若是找到比当前key上的值小的值,则替换key值 key = j; } if(key!=i) // key == i说明 无序序列中没有比a[i]更小的数了,没必要替换 { tmp = a[i]; // 替换a[i]和无序序列中最小的元素a[key] a[i] = a[key]; a[key] = tmp; } } return; }
堆排序是一种树形选择排序,是对直接选择排序的有效改进。实际上也是一种选择排序,只不过采用了堆这种数据结构,利用这种数据结构,使得在每次查找最大元素时,直接选取堆顶元素,从而时间大大缩短,相对简单选择排序算法来讲,比较高效。优化
将初始待排序关键字序列(A0, A1, A2 .... An-1)构建成大顶堆(从最后一个非叶子结点 i = len/2 - 1 自下而上),此堆为初始的无序区(构建堆)
将堆顶元素A[0]与最后一个元素A[n-1]交换,此时获得新的无序区(A0, A1, A2,......An-2)和新的有序区(An-1) (交换首尾元素)
因为交换后新的堆顶A[0]可能违反堆的性质,所以须要对当前无序区(A0,A1,......An-2)从新调整为新的大顶堆(从根节点 i = 0,自上而下) (调整堆)
而后再次将A[0]与无序区最后一个元素交换,获得新的无序区(A0,A1....Rn-3)和新的有序区(An-2,An-1)。(不断循环)
不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成
// 下标从0开始,左孩子是2start+1; 若是从1开始,是2start
void HeapAdjust(int *A, int start, int end) { int k; int tmp = A[start]; for(k=2*start+1; k<=end; k=k*2+1) // k=k*2+1 是取左孩子 { if(k<end && A[k]<A[k+1]) // 比较左右孩子,选较大者 k++; if(tmp<A[k]) // 比较父节点和左右孩子中较大者,若是孩子较大,将孩子节点值赋值给父节点,start指向孩子节点的位置 { A[start] = A[k]; start = k; } else break; } A[start] = tmp; // 将一开始的start的值赋给此时start指向的节点,即将原来不和谐的元素放到合适的位置上 } void HeapSort(int *A, int len) { int i, j; for(i=len/2-1; i>=0; i--) //从最后一个非叶子结点i(len/2-1)从下至上,从右至左调整结构,构建初始堆 { HeapAdjust(A, i, len); } for(j=len-1; j>=0; j--) // 将堆顶元素和末尾元素互换,再将剩下的j-1个元素调整为大顶堆 { int tmp = A[j]; A[j] = A[0]; A[0] = tmp; HeapAdjust(A, 0, j-1); // 重建堆是从顶至下,与初始堆相反 } }
http://www.javashuo.com/article/p-csojmyyk-gg.html 堆排序详解
http://www.cnblogs.com/mengdd/archive/2012/11/30/2796845.html 堆排序 Heap Sort
冒泡排序是一种相对简单的排序,它每次比较相邻的两个元素,若是前者大于后者,则交换< swap >这两个元素(从小到大排序),这样每一趟比较就把大的元素沉入最后,形象的称之为“冒泡”,每走一趟,实际上最尾的元素已经排好。
将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;( 第一轮结束后,序列最后一个元素必定是当前序列的最大值;)
对序列当中剩下的n-1个元素再次执行步骤1。
对于长度为n的序列,一共须要执行n-1轮比较。(利用while循环能够减小执行次数)
void BubbleSort(int *A, int len) { int i, j; int tmp; for(i=0; i<len-1; i++) // 长度为len的数组,最多须要len-1次冒泡,便可有序(剩下一个自动变为最大或最小) { for(j=0; j<len-i-1; j++) // 注意:每次循环比较len-1-i次,若是写成len-1,多了一次数组会越界,产生不可预料的结果 { if(A[j]>A[j+1]) { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; } } } }
对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程当中是否有数据交换,若是进行某一趟排序时并无进行数据交换,则说明数据已经按要求排列好,可当即结束排序,避免没必要要的比较过程。
// 改进一,加入标志位 void BubbleSort2(int *A, int len) { int i, j; int tmp; int flag; // flag=1时表示已经排序完成 for(i=0; i<len-1; i++) { flag = 1; // 每次冒泡前把flag置1 for(j=0; j<len-i-1; j++) { if(A[j]>A[j+1]) { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; flag = 0; // 若是发生交换,置0,说明尚未排序完成 } } if(flag==1) // 若是一次冒泡循环中一次交换也没发生,则flag仍是为1,说明数组已经排好序 break; } }
在冒泡排序的每趟扫描中,记住最后一次交换发生的位置lastexchange也能有所帮助。由于该位置以前的相邻记录已经有序,故下一趟排序开始的时候,0到lastexchange已是有序的了,lastexchange到n-1是无序区。因此一趟排序可能使当前有序区扩充多个记录.即较大缩小无序区范围,而非递减1,以此减小排序趟数。这种算法以下:
// 改进二,记录最后一次交换的位置 void BubbleSort3(int *A, int len) { int j; int tmp; int flag = len - 1; int pos; while(flag>0) { pos = 0; // 不要漏了该条赋值语句。不然会陷入无限循环。 for(j=0; j<flag; j++) { if(A[j]>A[j+1]) // 若是没有交换,则不会进入该if分支,pos仍是=0,说明已经排好序,while循环才会结束 { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; pos = j; // 记录最后一次交换的位置 } } flag = pos; // 把位置赋给flag,用于判断无序区是否还有元素 } }
传统冒泡排序中每一趟排序操做只能找到一个最大值或最小值,咱们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次能够获得两个最终值(最大者和最小者) , 从而使排序趟数几乎减小了一半。
// 改进三,双向冒泡 void BubbleSort4(int *A, int len) { int low=0, high=len-1; int i; int tmp; while(low<high) { for(i=low; i<high; i++) // 正向冒泡,找到最大者 { if(A[i]>A[i+1]) { tmp = A[i]; A[i] = A[i+1]; A[i+1] = tmp; } } high--; // 修改high值, 前移一位 for(i=high; i>low; i--) // 反向冒泡,找到最小者 { if(A[i]<A[i-1]) { tmp = A[i]; A[i] = A[i-1]; A[i-1] = tmp; } } low++; // 修改low值,后移一位 } }
快速排序(Quicksort)是对冒泡排序的一种改进。
关于快速排序,它的基本思想就是选取一个基准,一趟排序肯定两个区间,一个区间所有比基准值小,另外一个区间所有比基准值大,接着再选取一个基准值来进行排序,以此类推,最后获得一个有序的数列。
1.选取基准值,经过不一样的方式挑选出基准值。
2.用分治的思想进行分割,经过该基准值在序列中的位置,将序列分红两个区间,在准值左边的区间里的数都比基准值小(默认以升序排序),在基准值右边的区间里的数都比基准值大。
3.递归调用快速排序的函数对两个区间再进行上两步操做,直到调用的区间为空或是只有一个数。
// 基本双向快速排序 void QuickSort(int *A, int start, int end) { if(start<end){ // 调试时少了这一步,一直报错 int i=start, j=end; int pivot = A[i]; // 第0个元素做为基准数 while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基准数归位,i左边为较小数,右边为较大数 QuickSort(A, start, i-1); // 递归调用,将剩下两部分继续进行快排 QuickSort(A, i+1, end); } }
上面版本的快排在选取主元的时候,每次都选取第一个元素。当序列为有序时,会发现划分出来的两个子序列一个里面没有元素,而另外一个则只比原来少一个元素。为了不这种状况,引入一个随机化量来破坏这种有序状态。
// 随机选取基准数,若是序列基本有序,能够避免分治以后一个区间元素过少,一个区间元素过多的状况 void QuickSort2(int *A, int start, int end) { if(start<end){ int i=start, j=end; int pivot_pos = rand() % (end - start) + start; // 从start~end间随机选取一个元素做为基准数 int pivot = A[pivot_pos]; // 开辟一块内存存储pivot值 SWAP(&A[pivot_pos], &A[start]); // 交换pivot_pos和start位置上的值,不会影响到上面pivot的值 while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基准数归位,i左边为较小数,右边为较大数 QuickSort2(A, start, i-1); // 递归调用,将剩下两部分继续进行快排 QuickSort2(A, i+1, end); } }
可是随机函数自己也要消耗必定的时间,并且随机选取也有可能出现很差分割的概率,因此又提出了三数取中法,即取左端、右端和中间三个元素排序后取中间的数做为关键元素。
// 三数取中选取基准数,选取序列头尾还有中间的三个数,取三个中值在中间的元素做为基准数 void QuickSort3(int *A, int start, int end) { if(start<end){ int mid = (start + end) / 2; if(A[mid]>A[end]) SWAP(&A[mid], &A[end]); if(A[start]>A[end]) SWAP(&A[start], &A[end]); if(A[start]<A[mid]) SWAP(&A[start], &A[mid]); //经过以上三步能够找到中间值,存放在A[start]中 int i=start, j=end; int pivot = A[start]; while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基准数归位,i左边为较小数,右边为较大数 QuickSort3(A, start, i-1); // 递归调用,将剩下两部分继续进行快排 QuickSort3(A, i+1, end); return; } else return; }
缘由:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到必定大小后,继续分割的效率比插入排序要差,此时可使用插排而不是快排
截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生相似的结果。这种作法也避免了一些有害的退化情形。
if (start - end + 1 < 10) { InsertSort(A, start, end); return; } //else时,正常执行快排
其实这种优化编译器会本身优化,相比不使用优化的方法,时间几乎没有减小。
QuickSort函数在其尾部有两次递归操做。若是待排序的序列划分极端不平衡,递归的深度将趋近于n,而不是平衡时的logn。
// 尾递归,减小递归深度 int Partition(int *A, int start, int end) { int pivot = A[start]; while(start<end) { while(start<end && A[end]>pivot) end--; SWAP(&A[end], &A[start]); while(start<end && A[start]<pivot) start++; SWAP(&A[start], &A[end]); } return start; } void QuickSortTail(int *A, int start, int end) { /* 普通递归方式,start和end这两个局部变量在下一次函数调用中还须要使用,因此须要继续堆栈 if(start<end) { int pivot_pos = Partition(A, start, end); QuickSort(A, start, pivot_pos-1); QuickSort(A, pivot_pos+1, end); } */ while(start<end) { int pivot_pos = Partition(A, start, end); if(pivot_pos-start < end-pivot_pos) // 短的部分采用递归,能够有效减小递归深度 { QuickSortTail(A, start, pivot_pos-1); // 左半部分继续递归 start = pivot_pos + 1; } else { QuickSortTail(A, pivot_pos+1, end); // 右半部分继续递归 end = pivot_pos - 1; } } }
归并排序是创建在归并操做上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个很是典型的应用。
分为两个函数实现:
一、分:能够看到这种结构很像一棵彻底二叉树,本文的归并排序咱们采用递归去实现(也可采用迭代的方式去实现)。分阶段能够理解为就是递归拆分子序列的过程,递归深度为log2n。
二、并:而后考虑下如何将将二个有序数列合并。这个很是简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。而后再进行比较,若是有数列为空,那直接将另外一个数列的数据依次取出便可。
// 把两个有序数组合并成一个有序数组 void MergeArray(int *A, int len_A, int *B, int len_B) { int i, j, k; i = j = k = 0; int temp[len_A + len_B]; while(i<len_A && j<len_B) //取两个数组第一个元素比较,小的存入temp中 { if(A[i]<B[j]) temp[k++] = A[i++]; else temp[k++] = B[j++]; } while(i<len_A) //两个数组中剩余的元素必定是偏大的,直接存入temp中 { temp[k++] = A[i++]; } while(j<len_B) { temp[k++] = B[j++]; } for(i=0; i<(len_A+len_B); i++) A[i] = temp[i]; //temp再转存入A数组中 } //递归实现归并排序 void MergeSort1(int *A, int len) { if(len>1) { // 数组分红两半 int *list1 = A; int list1_len = len/2; int *list2 = A + len/2; int list2_len = len - list1_len; MergeSort1(list1, list1_len); //左边部分递归 MergeSort1(list2, list2_len); //右边部分递归 MergeArray(list1, list1_len, list2, list2_len); //合并两个有序数组 } }
非递归的方法,避免了递归时深度为log2N的栈空间,空间只是用到归并临时申请的跟原来数组同样大小的空间,而且在时间性能上也有必定的提高,所以,使用归并排序是,尽可能考虑用非递归的方法。
不管是基于递归仍是循环的归并排序, 它们调用的核心方法都是相同的:完成一趟合并的算法,即两个已经有序的数组序列合并成一个更大的有序数组序列 (前提是两个原序列都是有序的)
//迭代实现归并排序 void MergeSort2(int *A, int len) { int left_min, left_max, right_min, right_max; int i, next; int *temp = (int *)malloc(len*sizeof(int)); if(temp == -1) return; for(i=1; i<len; i*=2) //步长从1开始,以2的倍数递增 { for(left_min=0; left_min<len-i; left_min=right_max) // 每次循环结束后,把letf_min指向right_max;right_max指向right_min+i; { right_min = left_max = left_min + i; right_max = right_min + i; if(right_max>len) // right_max 最大为len,若是超出,则等于len { right_max = len; } next = 0; while(left_min<left_max && right_min<right_max) // 合并[left_min, left_max]和[right_min, right_max]两个区间的数组 { if(A[left_min]<A[right_min]) { temp[next++] = A[left_min++]; } else { temp[next++] = A[right_min++]; } } while(left_min<left_max) // 若是是左边的元素还有剩余,则把它们移动到right数组的最右边;若是是右边的数组元素还有剩余,则不用动 { A[--right_min] = A[--left_max]; } while(next>0) // 再把temp数组填充到left和right数组中 { A[--right_min] = temp[--next]; } } } }
前面全部的排序算法都存在比较,均可以称为”比较排序“。比较排序的下界为o(nlogn)。那么有没有时间复杂度为o(n)的线性时间排序算法呢?计数排序即是很基础的一种线性时间排序,它是基数排序的基础。基本思想是:对每个元素x,肯定小于x的元素个数,就能够把x直接放到它在有序序列中的位置上。过程描述:假设待排序序列a中值的范围[0,k],其中k表示待排序序列中的最大值。首先用一个辅助数组count记录各个值在a中出现的次数,好比count[i]表示i在a中的个数。而后依次改变count中元素值,使count[i]表示a中不大于i的元素个数。而后从后往前扫描a数组,a中的元素根据count中的信息直接放到辅助数组b中。最后把有序序列b复制到a。
计数排序是稳定的排序算法;平均时间复杂度、最优时间复杂度和最差时间复杂度都为O(n+k),空间复杂度为O(n+k),其中,n为待排元素个数,k为待排元素的范围(0~k)。
// 计数排序 void CountSort(int *A, int len) { int min = A[0]; int max = A[0]; int i = 0; //找出数组中的最大值和最小值,肯定哈希表的大小 for(i=0; i<len; i++) { if(A[i]<min) min = A[i]; if(A[i]>max) max = A[i]; } int count_size = max - min + 1; int *count_arr = (int *)malloc(sizeof(int) * count_size); // 建立一个用于计数的数组 int *temp = (int *)malloc(sizeof(int) * len); for(i=0; i<count_size; i++) count_arr[i] = 0; // 计数数组所有初始化为0 for(i=0; i<len; i++) count_arr[A[i]-min]++; // 统计数组A中各元素出现的次数,并放在对应的位置上 for(i=1; i<count_size; i++) count_arr[i] += count_arr[i-1]; // 储存本身数组下标数值在目标数组对应的位置,保证稳定性 for(i=0; i<count_size; i++){ //printf("%d, ", count_arr[i]); } //printf("\n"); for(i=len-1; i>=0; i--) { temp[count_arr[A[i]-min]-1] = A[i]; //将原数组按大小顺序储存到另外一个数组 count_arr[A[i]-min]--; //temp[--count_arr[A[i] - min]] = A[i]; } for(i=0; i<len; i++) A[i] = temp[i]; free(count_arr); free(temp); }
不过该计数排序不能对负数进行排序,若是须要对负数进行排序,须要进行改进(先一次遍历待排数组,找出负数中的最大值和最小值,正数中的最大值和最小值,建立两个计数数组,negativeCountArray用来统计待排数组中各个不一样负数的出现个数,positiveCountArray用来统计待排数组中各个正数出现的个数,先统计总的负数的个数,而后再统计各个不一样正数的个数,而后在往sortedArray中放元素的时候对正数和负数区别对待。)
基数排序的基本思想是:一共有10个"桶",表明各个数位为0~9.在每一个桶上,组织一个优先队列,对于输入的一系列正整数和0,按照个位的大小关系分别进入10个"桶"中.而后遍历每一个"桶",按照十位的大小关系进行调整,紧接着是百位,千位.......直到到达最大数的最大位数。
基数排序只是针对于数字,思想就是将咱们须要待排列的元素按照指定的进制将每一位排列,时间复杂度为:P(N+B),注:其中P为待排列数字的最大位数,N为待排序列的长度,B为进制数。
//基数排序 void RadixSort(int *A, int len) { int max_digit = 0; int i, j; int radix = 10; //求数组中的最大位数 for (i = 0; i < len; ++i) { while (A[i] > (pow(10, max_digit))) { max_digit++; } } int flag = 1; for(j=1; j<=max_digit; j++) { //创建数组 统计每一个位上不一样数字出现的次数 int digit_count_arr[10] = { 0 }; for(i=0; i<len; i++) digit_count_arr[(A[i] / flag) % radix]++; //创建数组 统计A[i]在temp中的起始下标 int BeginIndex[10] = { 0 }; for(i=1; i<radix; i++) BeginIndex[i] = BeginIndex[i - 1] + digit_count_arr[i - 1]; //创建临时数组 用于收集数据 int *tmp = (int *)malloc(sizeof(int) * len); //初始化 for(i=0; i<len; i++) tmp[i] = 0; //将数据写入临时数组 for(i=0; i<len; i++) { int index = (A[i] / flag) % 10; tmp[BeginIndex[index]++] = A[i]; // 写入一个同时要把该位置上的计数加1,由于同一个位置可能有多个元素 } //将数据从新写回原数组 for(i=0; i<len; i++) A[i] = tmp[i]; flag = flag * 10; // 用于下一位的排序 free(tmp); } }
资料
http://www.javashuo.com/article/p-urxrwdjw-hv.html
经常使用七大经典排序算法总结(C语言描述)
http://blog.csdn.net/gl486546/article/details/53053069
排序算法之希尔排序
https://www.jianshu.com/p/7d037c332a9d
数据结构常见的八大排序算法(详细整理)
http://blog.csdn.net/zhangjikuan/article/details/49095533
九大排序算法-C语言实现及详解
https://www.jianshu.com/p/6777a3297e36
快速排序的优化 和关于递归的问题,说说个人想法
http://www.cnblogs.com/Anker/archive/2013/03/04/2943498.html
递归与尾递归总结
http://blog.csdn.net/touch_2011/article/details/6785881 漫谈经典排序算法:4、归并排序(合并排序)(推荐)