面试题笔记(一)- 排序算法
冒泡排序
原理
冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
算法描述
- 比较相邻的两个数,如果第一个数比第二个数大,则二者交换
- 对之后的相邻元素进行同样的工作,从开始到最后一对,这样进行一次排序后,数据的最后一位会是最大值
- 之后对所有的元素重复以上的步骤,直到排序完成
代码
void Bubble_Sort(int *pt, int N)
{
for (int i=N-1; i>0; i--){
int flag = 1;
for (int j=0; j<i; j++){
if (pt[j] > pt[j+1]){
swap(pt[j], pt[j+1]);
flag = 0;
}
}
if (flag == 1) break;
}
}
复杂度
- 平均复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:稳定
插入排序
原理
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间
算法描述
- 从第二个元素开始,该元素之前可以认为已经被排序
- 取出该元素,在已经排序的元素序列中从后向前扫描
- 如果该元素小于前一个元素,将前一个元素向后移动一个位置
- 重复步骤3,直到找到已排序的元素小于或者等于该元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
代码
void Insert_Sort(int *pt, int N)
{
for (int i=1; i<N; i++){
int temp = pt[i];
int j;
for (j=i; j>0 && temp<pt[j-1]; j--){
pt[j] = pt[j-1];
}
pt[j] = temp;
}
}
复杂度
- 平均复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:稳定
选择排序
原理
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法描述
- 从原序列中找到最小值,与数组第一个元素交换;
- 除第一个元素外,从剩下未排序的序列中找到最小值,与数组第二个元素交换;
- 共N-1趟,每趟都找到未排序的最小值,放到已排序的序列后面。
代码
void Select_Sort(int *pt, int N)
{
for (int i=0;i<N;i++){
int min = pt[i], num = i;
for (int j=i+1; j<N; j++){
if (min > pt[j]){
min = pt[j];
num = j;
}
}
pt[num] = pt[i];
pt[i] = min;
}
}
复杂度
- 平均复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:不稳定
希尔排序
简单排序算法的下界
-
逆序:具有性质 i < j 但 a[i] > a[j] 的序偶(a[i],a[j])。如序列34,8,64,51,32,21有9个逆序,即(34,8)、(34,32)、(34,31)、(64,51)、(64,32)、(64,21)、(51,32)、(51,21)以及(32,21)
-
注意:这正好是需要(隐含)执行的交换次数。事实总是这样,因为交换两个不按顺序排序的相邻元素恰好消除一个逆序,而一个排过序的数组没有逆序。由于算法中还有O(N)量的工作,假设 I 是原数组中的逆序数,所以插入排序的运行时间是O(I + N)。所以,如果逆序数是O(N),则插入排序以线性时间运行;冒泡排序通过加标志位也可以在有序的条件下达到最优O(N)。所以我们可以通过计算原始序列中的平均逆序数得出简单排序的平均时间精确的界。这里假设元素互异(如果允许重复,那我们连重复的平均次数都无法知道)。
定理1:N个互异数的数组的平均逆序数是N(N-1)/4。 定理2:通过交换相邻元素进行排序的任何算法平均都需要
-
结论:这个下界告诉我们,为了使一个排序算法以亚二次或O(N2)时间运行,必须执行一些比较,特别是要对相距较远的元素进行交换。一个排序算法通过删除逆序得以向前进行,而为了有效地进行它必须使每次交换删除不止一个逆序。
原理
通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。所以希尔排序也叫缩减增量排序。
算法思想
- 选择一个增量序列h1, h2, …. ht,h1 = 1
- 根据增量序列个数,即循环t次进行排序,每次排序结束后更换为ht-1的增量
- 把原数组分为ht个子数组,对每个子数组进行插入排序。
代码
// Origin序列
void Origin_Shell_Sort(int *pt, int N)
{
for (int P=N/2;P>0;P/=2){
for (int i=P;i<N;i++){
int temp = pt[i], j;
for (j=i;j>=P && temp<pt[j-P];j-=P){
pt[j] = pt[j-P];
}
pt[j] = temp;
}
}
}
// Sedgewick序列
void Sedgewick_Shell_Sort(int *pt, int N)
{
int Sedgewick[9] = {3905, 929, 505, 209, 109, 41, 19, 5, 1};
for (int k=0;k<9;k++){
int P = Sedgewick[k];
for (int i=P; i<N; i++){
int temp = pt[i], j;
for (j=i; j>=P && temp<pt[j-P]; j-=P){
pt[j] = pt[j-P];
}
pt[j] = temp;
}
}
}
// Hibbard序列
void Hibbard_Shell_Sort(int *pt, int N)
{
for (int k=log2(N);k>0;k--){
int P = pow(2,k)-1;
for (int i=P;i<N;i++){
int temp = pt[i], j;
for (j=i;j>=P && temp<pt[j-P];j-=P){
pt[j] = pt[j-P];
}
pt[j] = temp;
}
}
}
复杂度分析
- 希尔排序的运行时间依赖于增量序列的选择
- 使用希尔增量时希尔排序最坏时间复杂度是:O(n^2)。
- 使用Hibbard增量的希尔排序最坏时间复杂度是:O(n3/2);最优时间复杂度是O(n5/4)。
- 使用Sedgewick 增量序列,排序最坏时间复杂度是:O(n4/3);平均时间复杂度是O(n7/6)。最好的序列是{1,5,19,41,109……}。该序列中的项或者是9 * 4i - 9 * 2i +1的形式,或者是4i - 3* 2i+1的形式。
- 空间复杂度:只用到一个临时变量,所以空间复杂度为O(1);
- 稳定性:不稳定排序。因为每一趟的步长不一样,所以步长长的插入排序可能会把后面的元素插入到前面
堆排序
原理
通过将数组元素构建大顶堆,将顶部元素取出,此时堆尾部空出一个位置,将取出元素放置堆尾部,重复进行N-1次操作
代码
// 构建大顶堆
void PercMaxHeap(int *pt,int p, int N)
{
int parent, child, X = pt[p];
for (parent=p; parent*2+1<N; parent=child){
child = parent*2+1;
if (pt[child] < pt[child+1] && child+1 < N)
child++;
if (X > pt[child]) break;
else{
pt[parent] = pt[child];
}
}
pt[parent] = X;
}
// 堆排序
void Heap_Sort(int *pt, int N)
{
for (int p=N/2-1; p>=0; p--){
PercMaxHeap(pt, p, N);
}
for (int i=N; i>0; i--){
PercMaxHeap(pt,0,i);
int max = pt[0];
pt[0] = pt[i-1];
pt[i-1] = max;
}
}
复杂度
时间复杂度:O(NlogN)
空间复杂度:O(1)
稳定性:不稳定
归并排序
原理
采用分治法:
- 分治:递归地把当前序列平均分割成两半后,对每一半再进行分割直至无法分割为止
- 归并:指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作
代码
- 递归版
// 递归版
void Merge(int A[], int TempA[], int L, int R, int RightEnd)
{
int T = L, LeftEnd = R-1, N = RightEnd-L+1;
while (L <= LeftEnd && R <= RightEnd){
if (A[L] <= A[R]) TempA[T++] = A[L++];
else TempA[T++] = A[R++];
}
while (L<=LeftEnd) TempA[T++] = A[L++];
while (R<=RightEnd) TempA[T++] = A[R++];
for (int i=0;i<=N;i++){
A[RightEnd] = TempA[RightEnd];
RightEnd--;
}
}
void Msort(int A[], int Temp[], int L, int RightEnd)
{
int Center = (L+RightEnd)/2;
if (L<RightEnd){
Msort(A, Temp, L, Center);
Msort(A, Temp, Center+1, RightEnd);
Merge(A, Temp, L, Center+1, RightEnd);
}
}
void Merge_Sort(int *pt, int N)
{
int *TempA = (int*)malloc(N*sizeof(int));
if (TempA!=NULL){
Msort(pt, TempA, 0, N-1);
free(TempA);
}
}
- 循环版
// 循环版
void MergeMinus(int A[], int TempA[], int L, int R, int RightEnd)
{
int T = L, LeftEnd = R-1;
while (L<=LeftEnd && R<=RightEnd){
if (A[L]<=A[R]) TempA[T++] = A[L++];
else TempA[T++] = A[R++];
}
while (L<=LeftEnd) TempA[T++] = A[L++];
while (R<=RightEnd) TempA[T++] = A[R++];
}
void CirMerge(int A[], int TempA[], int N, int length)
{
int i,j;
for (i=0; i<N-2*length; i+=2*length){
MergeMinus(A, TempA, i, i+length, i+2*length-1);
}
if (i+length < N)
MergeMinus(A, TempA, i, i+length, N-1);
else
for (j=i; j<N; j++) TempA[j] = A[j];
}
void CirMerge_Sort(int *pt, int N)
{
int *TempA = (int*)malloc(N*sizeof(int));
int length = 1;
if (TempA!=NULL){
while (length<N){
CirMerge(pt, TempA, N, length);
length *= 2;
CirMerge(TempA, pt, N, length);
length *= 2;
}
free(TempA);
}
}
复杂度
时间复杂度:O(NlogN)
空间复杂度:O(N)
稳定性:稳定
应用
归并排序经常应用于外部大数据排序,具体操作可以参考这篇文章 外部排序
快速排序
算法思想
- 初始化两个索引 low 和 high,分别指向数组第一个与倒数第一个元素
- 选取枢纽元 pivot, 并将枢纽元与数组倒数第二个元素调换,使枢纽元离开要被分割的数据段;
- 如果 low 索引指向的元素小于枢纽元,则 low++;否则,low 停止。high 索引指向的元素大于枢纽元,high–;否则,high 停止。
- 如果 low < high,则交换两个元素,循环继续3,4步骤;否则跳出循环,将 low 对应的元素与枢纽元交换(这时候完成了分割)。递归调用这两个子序列
代码
void Swap(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int Median3(int pt[], int left, int right)
{
int center = (left+right)/2;
if (pt[left]>pt[center])
Swap(&pt[left], &pt[center]);
if (pt[left]>pt[right])
Swap(&pt[left], &pt[right]);
if (pt[center]>pt[right])
Swap(&pt[center], &pt[right]);
Swap(&pt[center], &pt[right-1]);
return pt[right-1];
}
// 插入排序
void QInsert_Sort(int *pt, int Left, int Right)
{
for (int i=Left;i<=Right;i++){
int temp = pt[i], j;
for (j=i;j>0 && temp<pt[j-1];j--){
pt[j] = pt[j-1];
}
pt[j] = temp;
}
}
void Qsort(int A[], int Left, int Right)
{
int CutOff = 100; // 当元素数目小于100时,使用插入排序
if (CutOff<=Right-Left){
int Pivot = Median3(A, Left, Right);
int Low = Left, High = Right-1;
while (1){
while (A[++Low]<Pivot);
while (A[--High]>Pivot);
if (Low<High)
Swap(&A[Low], &A[High]);
else
break;
}
Swap(&A[Low], &A[Right-1]);
Qsort(A, Left, Low-1);
Qsort(A,Low+1, Right);
}
else
QInsert_Sort(A, Left, Right);
}
void Quick_Sort(int *pt, int N)
{
Qsort(pt, 0, N-1);
}
复杂度
- 最坏时间复杂度:元素全部逆序,时间复杂度为O(N2)。
- 最好时间复杂度:每次序列都能均分为两个子序列,时间复杂度是O(NlogN)
- 平均时间复杂度:O(NlogN)。
- 空间复杂度:O(logN)