引入
经过了两趟面试,我意识到自己对以前学习过算法,尤其是排序算法已经遗忘不少了,是时候再复习一下。
排序的概念
首先,什么是排序?
将记录序列排列成按关键字有序的序列的操作,称为排序(Sort)。其中排序依据的关系是任意的,通常使用小于(递增)、大于(递减)的关系。————张琨 张宏 朱保平《数据结构与算法分析(C++语言版)》
稳定与不稳定的概念
面试官往往会问”某某排序是否稳定?“这样的概念,那什么叫做排序算法稳定,而什么是不稳定呢?
当待排序元素的关键字各不相同时,排序的结果是唯一的,否则排序的结果可能不唯一。如果待排序列中存在多个关键字相同的元素,它们之间的相对次序保持不变,则这种排序方法是稳定的;若排序后,这些具有相同关键字的元素之间的相对顺序发生了变化,则称是”不稳定“的。————张琨 张宏 朱保平《数据结构与算法分析(C++语言版)》
时间复杂度和空间复杂度的概念
面试官往往还会问某个排序算法的时间复杂度和空间复杂度的问题,那什么是时间复杂度和空间复杂度呢?
一个程序的时间复杂度(Time Complexity)是指程序运行从开始到结束所需要的时间。
一个程序的空间复杂度(Space Complexity)是指程序运行从开始到结束所需的存储空间的度量。
————张琨 张宏 朱保平《数据结构与算法分析(C++语言版)》
以下给出所有的排序算法的空间复杂度、时间复杂度、还有稳定性:
介绍完了概念,现在切入正题。排序算法可以分为三大类:插入类排序、交换类排序还有选择类排序。
四类插入排序
插入排序的原理是每次价格你一个待排序的数据按其关键字的大小插入到一个已经完成排序的有序序列中,直到全部序列排序结束。先来介绍四类插入排序:直接插入排序、折半插入排序、表插入排序、希尔排序。
直接插入排序
直接插入排序(Straight Insertion Sort)的算法思路是:通过构建有序的序列,对于未排序数据,在已排序序列中从后向前遍历,找到相应的位置插入。在从后向前遍历的过程中,需要反复把已经排序的元素逐步后移,为待插入的元素移出空间。
#include<iostream>
using namespace std;
int a[100];
void SInsertSort(int a[],int len)
{
for (int i = 2; i <= len; i++)
{
if (a[i] < a[i - 1])
{
a[0] = a[i];
a[i] = a[i - 1];
int j;
for (j = i - 2; a[0] < a[j]; j--)
a[j + 1] = a[j];
a[j + 1] = a[0];
}
}
}
int main(void)
{
for (int i = 10; i >=1; i--)a[i] = 10-i;
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
cout << endl;
SInsertSort(a, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
system("pause");
return 0;
}
注:下面的代码用函数替代上面的SInsertSort函数即可,不在重复书写main函数。基于直接插入排序算法,并对插入的策略进行改进而得到性的插入排序算法:折半插入排序和表插入排序。
折半插入排序
当待排序的数量很小时,直接插入排序是一种效率很高的算法。但是待排序的数量很大时,使用直接插入排序就不是那么乐观了。直接插入排序是通过向有序表中插入一个元素,其插入的位置是通过逐个比较有序表中的现有元素得到的。既然是在有序表中确定插入的位置,不妨使用二分法来确定插入的位置。即在一次比较中,通过比较待插入的元素和有序表中的元素,将有序表一分为二,下一次在有可能范围的那个有序表中继续比较,再二分,以此类推······直到确定位置为止。
略去main函数,给出了折半插入算法的C++实现代码,如下所示:
void BInsertSort(int a[], int len)
{
int high, low, m;
for (int i = 2; i <= len; i++)
{
a[0] = a[i];
low = 1;
high = i - 1;
while (low <= high)
{
m = (low + high) / 2;
if (a[0] < a[m])high = m - 1;
else low = m + 1;
}
for (int j = i - 1; j >= high + 1; j--)
a[j + 1] = a[j];
a[high + 1] = a[0];
}
}
表插入排序
前面两种插入排序算法都需要大量移动顺序表中的元素,不同于它们,表插入排序是一种不移动元素,通过改变存储结构来使得有序化的算法。主要思想是改变指针的指向,按关键字大小实现从小到大链接的过程。
代码较为复杂且笔者水平有限,这里略去。
希尔排序
希尔排序(Shell Sort)又称“缩小增量排序”,由D.L.Shell提出而得名,是对直接插入排序改进的一种排序算法。
希尔排序的基本思想:先将整个待排序序列分成若干个子序列,在子序列内分别进行直接插入排序;直到整个序列基本有序时再对全体元素进行一次直接插入排序。希尔排序与直接插入排序的区别在于,希尔排序不是每次比较都要一个个挨着比较过去,而是初期选用大跨步间隔比较,然后逐步缩小,直到为跨步1。
具体实现如下:
#include<iostream>
using namespace std;
int a[100];
void ShellInsert(int a[], int dk, int len)
{
for (int i = dk + 1; i <= len; i++)
{
if (a[i] < a[i - dk])
{
a[0] = a[i];
int j = i - dk;
for (; j > 0 && a[0] <= a[j]; j -= dk)
a[j + dk] = a[j];
a[j + dk] = a[0];
}
}
}
void ShellSort(int a[], int len)
{
for (int k = 0; k < len; k++)
ShellInsert(a, a[k], len);
}
int main(void)
{
for (int i = 10; i >= 1; i--)a[i] = 10 - i;
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
cout << endl;
ShellSort(a, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
system("pause");
return 0;
}
至此,四类插入排序,完。
两类交换排序
什么是交换排序?
交换排序是一类借助比较和交换进行的排序方法。其中交换是指对序列中两个记录的关键字进行比较,如果顺序不对则对换两个记录再序列中的位置。交换排序的特点是:将关键字较大的记录向序列的一端移动,而关键字小的向序列的另一端移动。
————张琨 张宏 朱保平《数据结构与算法分析(C++语言版)》
冒泡排序
冒泡排序(Bubble Sort)也称起泡排序,其基本思想是:通过对待排序元素中相邻元素间关键字的比较和交换,使关键字最大的元素如气泡一样逐渐“上浮”。
void BubbleSort(int a[], int len)
{
for (int i = 1; i < len; i++)
{
for (int j = 0; j <= len - i; j++)
{
if (a[j] > a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
快速排序
快速排序(Quick Sort)是1962年由Hore提出的一种排序算法。也称“分区交换排序”。
快速排序的基本思想:通过对关键字的比较和交换,以待排序序列中的某个数据为“支点”,或者称“枢轴量”。将待排序序列划分成两个部分,其中左半边小于等于支点,右半部分数据大于等于支点。然后,对左右两个部分分别进行快速排序的递归处理,直到整个序列有序为止。
在冒泡排序中,元素的比较和移动是在相邻位置进行的,元素每次交换只能前移或者后移一个位置,因而总的比较次数和移动次数较多。在快速排序中,元素的比较和移动是从两端向中间进行的,关键字较大的元素一次就能从前面移动到后面,关键字较小的元素一次就能从后面移到前面,移动距离较远,这样就减少了比较和移动的次数。因此,快排是对冒泡排序的一种改进。
#include<iostream>
using namespace std;
int a[100];
int Partition(int a[], int low, int high)
{
int pivotkey;//枢轴记录
a[0] = a[low];//a[0]是空的,1~10存放在a[1]~a[10]
pivotkey = a[low];//子表第一个元素率先作为枢轴记录
while (low < high)//low和high交替向中间扫描
{
while (low < high && a[high] >= pivotkey)--high;
a[low] = a[high];//比枢轴小的移动到低端
while (low < high && a[low] <= pivotkey)++low;
a[high] = a[low];//比枢轴大的移动到高端
}
a[low] = a[0];
return low;
}
void QuickSort(int a[], int low, int high)
{
int mid;//用于存放两个区间中间所夹着的点
if (low < high)
{
mid = Partition(a, low, high);
//递归
QuickSort(a, low, mid - 1);//夹着的点的左端
QuickSort(a, mid + 1, high);//夹着的点的右端
}
}
int main(void)
{
for (int i = 10; i >= 1; i--)a[i] = 10 - i;
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
cout << endl;
QuickSort(a, 1, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
system("pause");
return 0;
}
至此,两类交换排序完,还有一个选择排序。
选择排序
选择排序是一类借助“选择”进行排序的算法。
选择排序的基本思想:每一趟从待排序列中选取一个元素关键字最小的,第一趟从n个元素中选取最小的记录,第二趟从剩下的n-1个记录中选取关键字最小的记录,直到全部元素有序。
————张琨 张宏 朱保平《数据结构与算法分析(C++语言版)》
选择排序主要有以下几大类:简单选择排序、堆排序、归并排序、基数排序等,先看简单选择排序。
简单选择排序
简单选择排序是选择排序中最简单的一种排序算法。简单选择排序的基本思想:第一趟从n个元素中选出关键字最小的和第一个交换;第二趟从第二个元素开始的n-1个元素中再选出关键字最小的元素与第二个元素交换;如此第i趟则从第i个元素开始的n-i+1个记录中选出关键字最小的元素与第i个元素交换,直到整个序列有序。
#include<iostream>
using namespace std;
int a[100];
int SelectMinKey(int a[],int n,int len)
{
int min = n;
int minkey = a[n];
for(int i=n+1;i<=len;i++)
if (a[i] < minkey)
{
minkey = a[i];
min = i;
}
return min;
}
void SelectSort(int a[], int len)
{
int j, temp;
for (int i = 1; i <= len; i++)
{
j = SelectMinKey(a, i, len);
if (i != j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
int main(void)
{
for (int i = 10; i >= 1; i--)a[i] = 10 - i;
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
cout << endl;
SelectSort(a, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
system("pause");
return 0;
}
堆排序
堆排序(Heap Sort)是利用堆的特性进行排序的方法。堆是一个完全二叉树,顺序表里存放的元素的顺序就是这个完全二叉树层次遍历的顺序。大顶堆和小顶堆的定义如下:
- 每个节点的值都小于或者等于其左右孩子节点的值,称为小顶堆。
- 每个节点的值都大于或者等于其左右孩子节点的值,称为大顶堆。
堆排序的基本思想是:首先用待排序列的元素构造出一个堆,此时选出堆中所有元素的最小值作为堆顶。随后将其与堆中最后一个元素交换(或者取出)。接着将剩余的元素继续调整成堆,这样又可以找到第二小的元素,以此类推。代码如下:
#include<iostream>
using namespace std;
int a[100];
void HeapAdjust(int a[], int s, int m)
{
int rc = a[s];
for (int j = 2 * s; j <= m; j *= 2)
{
if (j < m && a[j] <= a[j + 1])j++;//找到值相对更大的孩子节点,并以此继续向下筛选
if (rc > a[j])break;//如果rc最大,退出循环
a[s] = a[j];
s = j;//交换位置
}
a[s] = rc;
}
void HeapSort(int a[], int len)
{
int value;
for (int i = len / 2; i > 0; i--)HeapAdjust(a, i, len);
for (int i = len; i > 1; i--)
{
value = a[1];
a[1] = a[i];
a[i] = value;
HeapAdjust(a, 1, i - 1);
}
}
int main(void)
{
for (int i = 10; i >= 1; i--)a[i] = 10 - i;
//int a[11] = { 0,0,9,4,5,6,7,2,1,3,8 };
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
cout << endl;
HeapSort(a, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << a[i] << ' ';
system("pause");
return 0;
}
归并排序
归并排序(merge Sort)是一类借助“归并”进行排序的方法。归并的含义是将两个或两个以上的有序序列归并成一个有序序列的过程。归并排序按所合并的表的个数可以分为二路归并排序和多路归并排序,这里以二路归并排序为例。
二路归并排序(2-way MErge Sort)的基本思想是:将待排序的n个元素看成是n个有序的子序列,每个子序列长度为1.然后两两归并,得到子序列长度为2或者1。再两两归并得到有序子序列长度为4或者3,以此类推直到完全有序。代码如下:
#include<iostream>
using namespace std;
int a[100];
void Merge(int* SR, int* TR,int i, int m, int n)
{
int k,j;
for (j = m + 1, k = i; i <= m && j <= n; k++)
{
if (SR[i] <= SR[j])TR[k] = SR[i++];
else TR[k] = SR[j++];
}
if (i <= m)
for (int a = i; a <= m; a++)
TR[k++] = SR[a];
else if (j <= n)
for (int b = j; b <= n; b++)
TR[k++] = SR[b];
}
//每一趟的归并
void MergeSort(int* SR, int *TR1, int s, int t)
{
int TR2[100];
int m;
if (s == t)TR1[s] = SR[s];
else
{
m = (s + t) / 2;
MergeSort(SR, TR2, s,m);//前一半
MergeSort(SR, TR2, m + 1, t);//后一半
Merge(TR2, TR1, s, m, t);//把TR2两段归并,结果放到TR1中去
}
}
int main(void)
{
//for (int i = 10; i >= 1; i--)a[i] = 10 - i;
int b[11] = { 0,0,9,4,5,6,7,2,1,3,8 };//原序列
int result[11];//用于存放结果
cout << "排序前:";
for (int i = 1; i < 11; i++)
cout << b[i] << ' ';
cout << endl;
MergeSort(b, result, 1, 10);
cout << "排序后:";
for (int i = 1; i < 11; i++)
cout << result[i] << ' ';
system("pause");
return 0;
}
基数排序
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
由于笔者水平有限,代码略去。感兴趣可以参考这位博主:https://www.cnblogs.com/skywang12345/p/3603669.html
END