本文章参考:https://www.cnblogs.com/fnlingnzb-learner/p/9374732.html
老哥已经讲得很细致了,我根据内容做一下理解和总结,对文章内容有问题的朋友可以移步大佬文章。
1、算法概述
1、算法分类
1、非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
2、线性时间非比较列排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
2、算法比较
3、相关概念
1、稳定:说一个排序是稳定排序,原因就是因为如果排序之前a和b相等且a在b的前面,那么排完序之后a仍旧在b的前面。
2、不稳定:如果排序算法是不稳定的,那么排序之前a和b相等且a在b的前面,排完序之后a可能出现在b的后面
3、时间复杂度:时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。具体来说,在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N)f(N),那么时间复杂度为O(f(N))O(f(N))。
4、空间复杂度:空间复杂度是对一个算法在运行过程中临时占用得存储空间大小的量度,一般也作为问题规模n得函数,以数量级形式给出,记作:S(n) = O(g(n))
2、快速排序
原理:假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(类似哨兵,以6为例),那么在排序过程中,我们需要让比6小的数在6左边,比6大的数在6右边,像这样: 3 1 2 5 4 6 9 7 10 8
开始逐步介绍:
从图中我们可以看到:
left指针,right指针,base参照数。
其实思想是蛮简单的,就是通过第一遍的遍历(让left和right指针重合)来找到数组的切割点。
第一步:首先我们从数组的left位置取出该数(20)作为基准(base)参照物。(如果是选取随机的,则找到随机的哨兵之后,将它与第一个元素交换,开始普通的快排)
第二步:从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置(也就是将10赋给20),此时数组为:10,40,50,10,60, left和right指针分别为前后的10。
第三步:从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60, left和right指针分别为前后的40。
第四步:重复“第二,第三“步骤,直到left和right指针重合,最后将(base)放到40的位置, 此时数组值为: 10,20,50,40,60,至此完成一次排序。
第五步:此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大, 以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
代码:
//left为数组下标起始下标,right为数组末尾下标
void QuickSort(int arr[], int left, int right) {
if (left > right)return;
int i = left, j = right, base = arr[left];
while (i != j) {
while (arr[j] >= base && i < j) {
j--;//找到第一个比哨兵小的元素下标
}
while (arr[i] <= base && i < j) {
i++;//找到第一个比哨兵大的元素下标
}
if (i < j) {
swap(arr[i], arr[j]);//交换
}
}
arr[left] = arr[i];//将该元素放到数组起始处
arr[i] = base;//哨兵归位
QuickSort(arr, left, i - 1);
QuickSort(arr, i + 1, right);
}
3、冒泡排序
原理: 冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。
代码:
// length是数组长度
void BubbleSort(int arr[], int length) {
if (length <= 1) return;
for (int i = 0; i < length-1; i++) {
for (int j = 0; j < length-1-i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
return;
}
4、选择排序
原理:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
代码:
// length是数组长度
void SelectionSort(int arr[], int length) {
if (length <= 1) return;
int min;
for (int i = 0; i < length-1; i++) {
min = i;
for (int j = i + 1; j < length; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
swap(arr[i], arr[min]);
}
return;
}
5、插入排序
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。
原理
对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。
考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。直接插入排序对于最坏情况(严格递减的数组),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的数组),需要比较的次数是n-1,需要移位的次数是0。插入排序的时间复杂度是O(n^2),空间复杂度是O(1),同时也是稳定排序。
代码:
void InsertionSort(int arr[], int length) {
if (length <= 1) return;
for (int i = 1; i < length; i++) {
for (int j = i; j > 0; j--) {
if (arr[j - 1] > arr[j]) {
swap(arr[j - 1], arr[j]);
}
else break;
}
}
return;
}
6、归并排序
**思想:**归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
1、分而治之
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
代码:
void Merge(int arr[],int low,int mid,int high){
//low为第1有序区的第1个元素,i指向第1个元素, mid为第1有序区的最后1个元素
int i=low,j=mid+1,k=0; //mid+1为第2有序区第1个元素,j指向第1个元素
int *temp=new(nothrow) int[high-low+1]; //temp数组暂存合并的有序序列
if(!temp){ //内存分配失败
cout<<"error";
return;
}
while(i<=mid&&j<=high){
if(arr[i]<=arr[j]) //较小的先存入temp中
temp[k++]=arr[i++];
else
temp[k++]=arr[j++];
}
while(i<=mid)//若比较完之后,第一个有序区仍有剩余,则直接复制到t数组中
temp[k++]=arr[i++];
while(j<=high)//同上
temp[k++]=arr[j++];
for(i=low,k=0;i<=high;i++,k++)//将排好序的存回arr中low到high这区间
arr[i]=temp[k];
delete []temp;//删除指针,由于指向的是数组,必须用delete []
}
//用递归应用二路归并函数实现排序——分治法
void MergeSort(int arr[],int low,int high){
if(low<high){
int mid=(low+high)/2;
MergeSort(arr,low,mid);
MergeSort(arr,mid+1,high);
Merge(arr,low,mid,high);
}
}
7、希尔排序
思想: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
void ShellSort(int arr[], int length) {
if (length <= 1) return;
for (int div = length / 2; div >= 1; div /= 2) {
for (int k = 0; k < div; k++) {
for (int i = div + k; i < length; i+=div) {
for (int j = i; j > k; j -= div) {
if (arr[j] < arr[j - div]) swap(arr[j], arr[j - div]);
else break;
}
}
}
}
return ;
}
8、堆排序
堆排序的步骤分为三步:
1、建堆(升序建大堆,降序建小堆);
2、交换数据;
3、向下调整。
假设我们现在要对数组arr[]={8,5,0,3,7,1,2}进行排序(降序):
首先要先建小堆
排序:
//调整堆
void AdjustHeap(int arr[], int node, int length) {//node为要调整的节点编号,从0开始编号;length为堆长度
int index = node;
int child = 2 * index + 1;//左孩子,第一个节点编号为0
while (child < length) {
if (child + 1 < length&&arr[child] < arr[child + 1]) {
child++;
}
if (arr[index] >= arr[child]) break;
swap(arr[index], arr[child]);
index = child;
child = 2 * index + 1;
}
}
//建堆
void MakeHeap(int arr[], int length) {
for (int i = length / 2; i >= 0; --i) {
AdjustHeap(arr, i, length);
}
}
//堆排序
void HeapSort(int arr[], int length) {
MakeHeap(arr, length);
for (int i = length - 1; i >= 0; i--) {
swap(arr[i], arr[0]);
AdjustHeap(arr, 0, i);
}
}
9、基数排序
基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小。
它是根据关键字中各位的值,通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的。
1、LSD(低位到高位的排序)
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。
我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以0~9来表示的。
所以我们不妨把0~9视为10个桶。
我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是0,将这个数存入编号为0的桶中。
分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。
这时,得到的序列就是个位数上呈递增趋势的序列。
按照个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。
接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。