博主在学习过程中深感基础的重要,经典排序算法是数据结构与算法学习过程中重要的一环,这里对笔试面试最常涉及到的7种排序算法(包括插入排序、希尔排序、选择排序、冒泡排序、快速排序、堆排序、归并排序)进行了详解。每一种算法都有基本介绍、算法原理分析、算法代码。
插入排序
1)算法简介
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
2)算法描述和分析
具体算法描述如下:
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个或以下)。
3)算法代码
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;
}
}
希尔排序
1)算法简介
希尔排序,也称缩小增量排序算法,名称源于它的发明者Donald Shell,是插入排序的一种高速而稳定的改进版本。
2)算法描述
1、先取一个增量把元素分割成若干个子序列,对各子序列分别进行直接插入排序。
2、依次缩减增量再进行排序。
3、直至所取的增量足够小时,再进行一次直接插入排序。
希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(3/2)),但是现今仍然没有人能找出希尔排序的精确下界。
3)算法代码
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)算法描述
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)
3)算法代码
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;
}
}
}
}
选择排序
1)算法简介
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法。
2)算法描述和分析
1、初始状态:无序区为R[1..n],有序区为空,令i=0。
2、在无序区R[i..n-1]中选出关键字最小的记录 R[k],将它与无序区的第1个记录R[i]交换,交换之后R[0…i]就形成了一个有序区。
3、i++并重复第二步,直到i==n-1,数组有序化了。
3)算法代码
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;
}
}
归并排序
1)算法简介
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
2)算法描述和分析
归并排序具体算法描述如下(递归版本):
1、Divide: 把长度为n的输入序列分成两个长度为n/2的子序列。
2、Conquer: 对这两个子序列分别采用归并排序。
3、Combine: 将两个排序好的子序列合并成一个最终的排序序列。
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
3)算法代码
//将两个有序数列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;
}
堆排序
1)算法简介
堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。
堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间。
2)堆的定义
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个元素的次小值。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序。
3)堆的存储
一般用数组来表示堆,若根结点存在序号0处, i结点的父结点下标就为(i-1)/2。i结点的左右子结点下标分别为2*i+1和2*i+2。(注:如果根结点是从1开始,则左右孩子结点分别是2i和2i+1。)
4)堆排序的实现
实现堆排序需要解决两个问题:
1.如何由一个无序序列建成一个堆?
2.如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
先考虑第二个问题,一般在输出堆顶元素之后,视为将这个元素排除,然后用表中最后一个元素填补它的位置,自上向下进行调整:首先将堆顶元素和它的左右子树的根结点进行比较,把最小的元素交换到堆顶;然后顺着被破坏的路径一路调整下去,直至叶子结点,就得到新的堆。
我们称这个自堆顶至叶子的调整过程为“筛选”。从无序序列建立堆的过程就是一个反复“筛选”的过程。
5)构造初始堆
初始化堆的时候是对所有的非叶子结点进行筛选。最后一个非终端元素的下标是[n/2]向下取整,所以筛选只需要从第[n/2]向下取整个元素开始,从后往前进行调整。
比如,给定一个数组,首先根据该数组元素构造一个完全二叉树。然后从最后一个非叶子结点开始,每次都是从父结点、左孩子、右孩子中进行比较交换,交换可能会引起孩子结点不满足堆的性质,所以每次交换之后需要重新对被交换的孩子结点进行调整。
6)进行堆排序
堆排序是一种选择排序。建立的初始堆为初始的无序区。
排序开始,首先输出堆顶元素(因为它是最值),将堆顶元素和最后一个元素交换,这样,第n个位置(即最后一个位置)作为有序区,前n-1个位置仍是无序区,对无序区进行调整,得到堆之后,再交换堆顶和最后一个元素,这样有序区长度变为2。
不断进行此操作,将剩下的元素重新调整为堆,然后输出堆顶元素到有序区。每次交换都导致无序区-1,有序区+1。不断重复此过程直到有序区长度增长为n-1,排序完成。
由排序过程可见,若想得到升序,则建立大顶堆,若想得到降序,则建立小顶堆。
7)算法代码
// 输入数组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);
}
}
快速排序
快速排序是各种笔试面试最爱考的排序算法之一,且排序思想在很多算法题里面被广泛使用。是需要重点掌握的排序算法。
1)算法简介
快速排序是由东尼·霍尔所发展的一种排序算法。其基本思想是,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
2)算法描述和分析
快速排序使用分治法来把一个串(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)
最差空间复杂度 根据实现的方式不同而不同
我们选取数组的第一个元素作为主元,每一轮都是和第一个元素比较大小,通过交换,分成大于和小于它的前后两部分,再递归处理。
3)算法代码
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) |
|
******************
作者:hao_09
时间:2015/8/4
文章地址:http://blog.csdn.net/lsh_2013/article/details/47280135
******************