插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:
1. 从第一个元素开始,该元素可以认为已经被排序
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5. 将新元素插入到该位置后
6. 重复步骤2~5
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
void InsertSort(int a[], int n) { //循环变量 int i,j; //中间变量 int temp; for (i=1; i<n; i++) { temp=a[i]; //从后向前循环,将a[0]~a[i-1]中大于temp的值后移 for (j=i-1; j>=0&&a[j]>temp; j--) a[j+1]=a[j]; //将temp放入合适位置 a[j+1]=temp; } }
希尔排序,也称缩小增量排序算法,名称源于它的发明者Donald Shell,是插入排序的一种高速而稳定的改进版本。
1、先取一个增量把元素分割成若干个子序列,对各子序列分别进行直接插入排序。
2、依次缩减增量再进行排序。
3、直至所取的增量足够小时,再进行一次直接插入排序。
希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(3/2)),但是现今仍然没有人能找出希尔排序的精确下界。
void ShellSort(int a[], int n) { //循环变量 int i,j; //增量 int increment; //中间变量 int temp; for (increment=n/2; increment>0; increment/=2) { //在增量分割的子序列中进行插入排序 for (i=increment; i<n; i++) { temp=a[i]; for (j=i-increment; j>=0&&a[j]>temp; j-=increment) { //右移 a[j+increment]=a[j]; } a[j+increment]=temp; } } }
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。
最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差空间复杂度 总共O(n),需要辅助空间O(1)
void BubbleSort(int a[], int n) { int i,j; //中间变量 int temp; for (i=0; i<n; i++) { for (j=0; j<n-1-i; j++) { //交换 if (a[j+1]<a[j]) { temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; } } } }
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法。
1、初始状态:无序区为R[1..n],有序区为空,令i=0。
2、在无序区R[i..n-1]中选出关键字最小的记录 R[k],将它与无序区的第1个记录R[i]交换,交换之后R[0…i]就形成了一个有序区。
3、i++并重复第二步,直到i==n-1,数组有序化了。
void SelectSort(int a[], int n) { //循环变量 int i,j; //最小元素的下标 int mindex; //中间变量 int temp; for (i=0; i<n; i++) { mindex=i; for (j=i+1; j<n; j++) { if (a[j]<a[mindex]) { mindex=j; } } temp=a[i]; a[i]=a[mindex]; a[mindex]=temp; } }
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序具体算法描述如下(递归版本):
1、Divide: 把长度为n的输入序列分成两个长度为n/2的子序列。
2、Conquer: 对这两个子序列分别采用归并排序。
3、Combine: 将两个排序好的子序列合并成一个最终的排序序列。
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
//将两个有序数列a[first~mid]和a[mid+1~last]合并 void merge(int a[], int pTemp[], int first, int mid, int last) { int i=first,j=mid+1,k=first; while(i<=mid&&j<=last) { if (a[i]<a[j]) { pTemp[k++]=a[i++]; } else pTemp[k++]=a[j++]; } while(i<=mid) pTemp[k++]=a[i++]; while(j<=last) pTemp[k++]=a[j++]; for (i=first; i<=last; i++) a[i]=pTemp[i]; } //归并排序 void MSort(int a[], int pTemp[], int left, int right) { int Center; if (left<right) { Center=(left+right)/2; MSort(a,pTemp,left,Center);//左边有序 MSort(a,pTemp,Center+1,right);//右边有序 merge(a,pTemp,left,Center, right);//将两个有序数列合并 } } bool MergeSort(int a[], int n) { int *pTempArray; pTempArray=(int *)malloc(n*sizeof(int)); if (pTempArray==NULL) return false; MSort(a,pTempArray,0,n-1); free(pTempArray); return true; }
堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。
堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间。
n个元素的序列{k1,k2,…,kn}当且仅当满足下列关系之一时,称之为堆。
情形1:ki <= k2i 且ki <= k2i+1 (最小化堆或小顶堆)
情形2:ki >= k2i 且ki >= k2i+1 (最大化堆或大顶堆)
其中i=1,2,…,n/2向下取整;
若将和此序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。
由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
若在输出堆顶的最小值之后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序。
一般用数组来表示堆,若根结点存在序号0处, i结点的父结点下标就为(i-1)/2。i结点的左右子结点下标分别为2*i+1和2*i+2。(注:如果根结点是从1开始,则左右孩子结点分别是2i和2i+1。)
实现堆排序需要解决两个问题:
1.如何由一个无序序列建成一个堆?
2.如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
先考虑第二个问题,一般在输出堆顶元素之后,视为将这个元素排除,然后用表中最后一个元素填补它的位置,自上向下进行调整:首先将堆顶元素和它的左右子树的根结点进行比较,把最小的元素交换到堆顶;然后顺着被破坏的路径一路调整下去,直至叶子结点,就得到新的堆。
我们称这个自堆顶至叶子的调整过程为“筛选”。从无序序列建立堆的过程就是一个反复“筛选”的过程。
初始化堆的时候是对所有的非叶子结点进行筛选。最后一个非终端元素的下标是[n/2]向下取整,所以筛选只需要从第[n/2]向下取整个元素开始,从后往前进行调整。
比如,给定一个数组,首先根据该数组元素构造一个完全二叉树。然后从最后一个非叶子结点开始,每次都是从父结点、左孩子、右孩子中进行比较交换,交换可能会引起孩子结点不满足堆的性质,所以每次交换之后需要重新对被交换的孩子结点进行调整。
堆排序是一种选择排序。建立的初始堆为初始的无序区。
排序开始,首先输出堆顶元素(因为它是最值),将堆顶元素和最后一个元素交换,这样,第n个位置(即最后一个位置)作为有序区,前n-1个位置仍是无序区,对无序区进行调整,得到堆之后,再交换堆顶和最后一个元素,这样有序区长度变为2。
不断进行此操作,将剩下的元素重新调整为堆,然后输出堆顶元素到有序区。每次交换都导致无序区-1,有序区+1。不断重复此过程直到有序区长度增长为n-1,排序完成。
由排序过程可见,若想得到升序,则建立大顶堆,若想得到降序,则建立小顶堆。
// 输入数组A,堆的长度len,以及需要调整的节点i,调堆 void HeapAdjust(int A[], int len, int i) { int left=2*i+1;//结点i的左孩子 int right=2*i+2;//结点i的右孩子 int largest=i; int temp; while(left<len||right<len) { if (left<len&&A[left]>A[largest]) { largest=left; } if (right<len&&A[right]>A[largest]) { largest=right; } //如果最大值不是父结点 if (i!=largest) { //交换父结点和拥有最大值的子结点 temp=A[i]; A[i]=A[largest]; A[largest]=temp; //新的父结点,以备迭代调堆 i=largest; //新的子结点 left=2*i+1; right=2*i+2; } else break; } } //建堆 void BuildHeap(int A[], int len) { //最后一个非叶子结点 int begin=len/2-1; for (int i=begin; i>=0; i--) { HeapAdjust(A,len,i); } } //堆排序 void HeapSort(int A[], int n) { int temp; //建堆 BuildHeap(A,n); while(n>1) { //交换堆的第一个元素和最后一个元素 temp=A[n-1]; A[n-1]=A[0]; A[0]=temp; n--; //调堆 HeapAdjust(A,n,0); } }
快速排序是各种笔试面试最爱考的排序算法之一,且排序思想在很多算法题里面被广泛使用。是需要重点掌握的排序算法。
快速排序是由东尼·霍尔所发展的一种排序算法。其基本思想是,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串行(sub-lists)。
步骤为:
1、从数列中挑出一个元素,称为 "基准"(pivot),
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
在平均状况下,排序n个项目要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度 O(n^2)
最优时间复杂度 O(n log n)
平均时间复杂度 O(n log n)
最差空间复杂度 根据实现的方式不同而不同
我们选取数组的第一个元素作为主元,每一轮都是和第一个元素比较大小,通过交换,分成大于和小于它的前后两部分,再递归处理。
void QuickSort(int a[], int left, int right) { int i,j,v; if (left<right) { i=left; j=right; //以本次最左边的值为标准进行划分 v=a[i]; do { //从右向左找第一个小于标准位置j while(a[j]>v&&i<j) j--; if (i<j) { a[i]=a[j]; i++;//将第j个元素置于左端,并重置i } //从左向右找第一个大于标准位置i while(a[i]<v&&i<j) i++; if (i<j) { a[j]=a[i]; j--;//将第i个元素置于右端,并重置j } } while (i!=j); //将标准值放入它的最终位置 a[i]=v; //对标准值左半部分递归 QuickSort(a,left,i-1); //对标准值右半部分递归 QuickSort(a,i+1,right); } }
总结一下各种排序算法如下:
名称 |
时间复杂度 |
额外空间 |
稳定性 |
考点 |
插入排序 |
平均O(n^2) 最优O(n) 最差O(n^2) |
O(1) |
稳定 |
选择填空 各种时间复杂度 移动元素个数 |
希尔排序 |
最差O(n log n) 最优 O(n) |
O(n) |
不稳定 |
时间复杂度 比较次数 |
选择排序 |
O(n^2) |
O(1) |
不稳定 |
同插入排序 |
冒泡排序 |
O(n^2) 最优O(n) 最差O(n^2) |
O(1) |
稳定 |
时间复杂度 比较次数 单轮冒泡 |
快速排序 |
O(n log n) |
O(1) |
不稳定 |
时间复杂度 快排partition算法 |
堆排序 |
O(n log n) |
O(n) |
不稳定 |
时间复杂度 堆调整,建堆,堆排序,Top K问题 |
归并排序 |
平均O(nlogn) 最差O(nlogn) 最优O(n) |
O(n) |
|
版权声明:本文为博主原创文章,未经博主允许不得转载。
常见经典排序算法学习总结,附算法原理及实现代码(插入、shell、冒泡、选择、归并、快排等)
原文地址:http://blog.csdn.net/lsh_2013/article/details/47280135