转载至:https://www.cnblogs.com/fnlingnzb-learner/p/9374732.html

一、算法概述

0.1 算法分类

十种常见排序算法可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

0.2算法复杂度

0.3 相关概念

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

二、快速排序

假设我们现在对“6  1  2 7  9  3  4  5 10  8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:

3  1  2 5  4  6  9 7  10  8

在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6,递归对左右两个区间进行同样排序即可。想一想,你有办法可以做到这点吗?这就是快速排序所解决的问题。

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。它的平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2).

首先上图:

从图中我们可以看到:

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为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。

快速排序代码如下:

 1 //快速排序,随机选取哨兵放前面
2 void QuickSort(int* h, int left, int right)
3 {
4 if(h==NULL) return;
5 if(left>=right) return;
6
7 //防止有序队列导致快速排序效率降低
8 srand((unsigned)time(NULL));
9 int len=right-left;
10 int kindex=rand()%(len+1)+left;
11 Swap(h[left],h[kindex]);
12
13 int key=h[left],i=left,j=right;
14 while(i<j)
15 {
16 while(h[j]>=key && i<j) --j;
17 if(i<j) h[i]=h[j];
18 while(h[i]<key && i<j) ++i;
19 if(i<j) h[j]=h[i];
20 }
21
22 h[i]=key;
23
24 //QuickSort(&h[left],0,i-1);
25 //QuickSort(&h[j+1],0,right-j-1);
26
27 QuickSort(h,left,i-1);
28 QuickSort(h,j+1,right);
29 }

三、冒泡排序

1.原理

冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。

2.算法实现

 1 //冒泡排序
2 void BubbleSort(int* h,size_t len){
3 if(h==NULL)return;
4 if(len<=1)return ;
5 //i是次数,j是具体下标
6 for(int i=0;i<len-1;++i)
7 for(int j=0;j<len-1;++j)
8 if(h[j]>h[j+1])
9 Swap(h[j],h[j+1])
10 return ;
11 }

四、选择排序

选择排序也是一种简单直观的排序算法,它的工作原理很容易理解;初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

选择排序不稳:

举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法

算法实现:

 1 //选择排序
2 void SelectionSort(int* h,size_t len){
3 if(h==nullptr) return;
4 if(len<=1) return;
5
6 int minindex,i,j;
7 //i是次数,也即排好的个数;j是继续排
8 for(i=0;i<len-1;++i){
9 minindex=i;
10 for(j=i+1;i<len;++j){
11 if(h[j]<h[minindex]) minindex=j;
12 }
13 Swap(h[i],h[minindex]);
14 }
15 return;
16 }

五、插入排序

直接插入排序(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),同时也是稳定排序。

下面用一个具体的场景,直观地体会一下直接插入排序的过程:

场景:

现有一个无序数组,共7个数:89 45 54 29 90 34 68。

使用直接插入排序法,对这个数组进行升序排序。

89 45 54 29 90 34 68

45 89 54 29 90 34 68

45 54 89 29 90 34 68

29 45 54 89 90 34 68

29 45 54 89 90 34 68

29 34 45 54 89 90 68

29 34 45 54 68 89 90

算法实现:

//插入排序
void InsertSort(int* h,size_t len){
if(h==nullptr)return;
if(len<=1)return; int i,j;
//i是次数,也即排好的个数;j是继续排
for(i=1;i<len;++i)
for(j=i;j>0;--j)
if(h[j]<h[j-1])Swap(h[j],h[j-1]);
else break;
return ; }

六、归并排序

基本思想

  归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

分而治之

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列

  再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

算法实现:

 1 //合并两个序列
2 void mergeArray(int arr[],int first,int mid,int last,int temp[]){
3 int i=first;
4 int j=mid+1;
5 int m=mid;
6 int n=last;
7 int k=0;
8 while(i<=m && j<=n){
9 if(arr[i]<=arr[j])
10 temp[k++]=arr[i++];
11 else
12 temp[k++]=arr[j++];
13 }
14 while(i<=m)
15 temp[k++]=arr[i++];
16 while(j<=n)
17 temp[k++]=arr[j++];
18 for(i=0;i<k;i++)
19 arr[first+i]=temp[i];
20 }
21
22 void mySort(int arr[],int first,int last,int temp[]){
23 if(first<last){
24 int mid=(first+last)/2;
25 mySort(arr,first,mid,temp);
26 mySort(arr,mid+1,last,temp);
27 mergeArray(arr,first,mid,last,temp);
28 }
29 }
30 bool mergeSort(int arr[],int len){
31 int *p=new int[len];
32 if(nullptr==p)
33 return false;
34 mySort(arr,0,len-1,p);
35 delete[] p;
36 return true;
37 }

七、希尔排序

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现。

基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

  我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

算法实现:

 1 //希尔排序
2 void ShellSort(int* h,size_t len){
3 if(h==nullptr)return;
4 if(len<=1)return;
5
6 for(int div=len/2;div>=1;div/=2)
7 for(int k=0;k<div;++k)
8 for(int i=div+k;i<len;i+=div)
9 for(int j=i;j>k;j-=div)
10 if(h[j]<h[j-div])Swap(h[j],h[j-div]);
11 else break;
12 return;
13 }

八、堆排序

堆排序实际上是利用堆的性质来进行排序的,要知道堆排序的原理我们首先一定要知道什么是堆。 
堆的定义: 
堆实际上是一棵完全二叉树。 
堆满足两个性质: 
1、堆的每一个父节点都大于(或小于)其子节点; 
2、堆的每个左子树和右子树也是一个堆。 
堆的分类: 
堆分为两类: 
1、最大堆(大顶堆):堆的每个父节点都大于其孩子节点; 
2、最小堆(小顶堆):堆的每个父节点都小于其孩子节点; 
 
堆的存储: 
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如下图所示: 
 
堆排序: 
由上面的介绍我们可以看出堆的第一个元素要么是最大值(大顶堆),要么是最小值(小顶堆),这样在排序的时候(假设共n个节点),直接将第一个元素和最后一个元素进行交换,然后从第一个元素开始进行向下调整至第n-1个元素。所以,如果需要升序,就建一个大堆,需要降序,就建一个小堆。 
堆排序的步骤分为三步: 
1、建堆(升序建大堆,降序建小堆); 
2、交换数据; 
3、向下调整。 
假设我们现在要对数组arr[]={8,5,0,3,7,1,2}进行排序(降序): 
首先要先建小堆: 

堆建好了下来就要开始排序了: 

现在这个数组就已经是有序的了。

算法实现

 1 //调整堆
2 void adjust_heap(int* a,int node,int size){
3 int left=2*node+1;
4 int right=2*node+2;
5 int max=node;
6 if(left<size && a[left]>a[max]){
7 max=left;
8 }
9 if(right<size && a[right]>a[max]){
10 max=right;
11 }
12 if(max!=node){
13 swap(a[max],a[node]);
14 adjust_heap(a,max,size);
15 }
16 }
17 //堆排序
18 void heap_sort(int* a,int len){
19 for(int i=len/2;i>=0;--i)
20 adjust_heap(a,i,len);
21 for(int i=len-1;i>=0;i--){
22 swap(a[0],a[i]); //将当前最大的放置到数组末尾
23 adjust_heap(a,0,i); //将未完成排序的部分继续进行堆排序
24 }
25 }

九、基数排序

基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小

它是根据关键字中各位的值,通过对排序的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}。

接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。

 1 int maxbit(int data[],int n){
2 int d=1;//保存最大的位数
3 int p=10;
4 for(int i=0;i<n;++i){
5 while(data[i]>=p){
6 p*=10;
7 ++d;
8 }
9 }
10 return d;
11 }
12
13 void radixsort(int data[],int n){//基数排序
14 int d=maxbit(data,n);
15 int tmp[n];
16 int count[10];//计数器
17 int i,j,k;
18 int radix=1;
19 for(i=1;i<=d;++i){
20 for(j=0;j<10;++j)
21 count[j]=0;//每次分配前清空计数器
22 for(j=0;i<n;j++){
23 k=(data[j]/radix)%10;//统计每个桶中的记录数
24 count[k]++;
25 }
26 for(j=1;j<10;++j)
27 count[j]=count[j-1]+count[j];//将tmp中的位置依次分配给每个桶
28 for(j=n-1;j>=0;j--){
29 k=(data[j]/radix)%10;
30 tmp[count[k]-1]=data[j];
31 count[k]--;
32 }
33 for(j=0;j<n;++j)//将临时数组的内容复制到data中
34 data[j]=tmp[j];
35 radix=radix*10;
36 }
37 }

转:C++经典排序算法总结的更多相关文章

  1. 经典排序算法 – 插入排序Insertion sort

    经典排序算法 – 插入排序Insertion sort  插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕. 插入排序方法分直接插入排序和折半插入排序两种, ...

  2. 经典排序算法总结与实现 ---python

    原文:http://wuchong.me/blog/2014/02/09/algorithm-sort-summary/ 经典排序算法在面试中占有很大的比重,也是基础,为了未雨绸缪,在寒假里整理并用P ...

  3. 经典排序算法及python实现

    今天我们来谈谈几种经典排序算法,然后用python来实现,最后通过数据来比较几个算法时间 选择排序 选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理是每一次从待排序的数据 ...

  4. 经典排序算法 - 基数排序Radix sort

    经典排序算法 - 基数排序Radix sort 原理类似桶排序,这里总是须要10个桶,多次使用 首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,临时忽视十位数 比如 待排序数组[ ...

  5. 经典排序算法 - 高速排序Quick sort

    经典排序算法 - 高速排序Quick sort 原理,通过一趟扫描将要排序的数据切割成独立的两部分,当中一部分的全部数据都比另外一部分的全部数据都要小,然后再按此方法对这两部分数据分别进行高速排序,整 ...

  6. 经典排序算法 - 归并排序Merge sort

    经典排序算法 - 归并排序Merge sort 原理,把原始数组分成若干子数组,对每个子数组进行排序, 继续把子数组与子数组合并,合并后仍然有序,直到所有合并完,形成有序的数组 举例 无序数组[6 2 ...

  7. C# 经典排序算法大全

    C# 经典排序算法大全 选择排序 using System; using System.Collections.Generic; using System.Linq; using System.Tex ...

  8. Jerry 2017年的五一小长假:8种经典排序算法的ABAP实现

    2017年4月29日~5月1日,国际劳动节, 三天的小长假. 在国内,小长假往往是这样的: 然而我当时在戏称为"德村"(德国农村)的Walldorf出差并且住在Wiesloch, ...

  9. 【最全】经典排序算法(C语言)

    算法复杂度比较: 算法分类 一.直接插入排序 一个插入排序是另一种简单排序,它的思路是:每次从未排好的序列中选出第一个元素插入到已排好的序列中. 它的算法步骤可以大致归纳如下: 从未排好的序列中拿出首 ...

  10. 【刷题】【LeetCode】000-十大经典排序算法

    [刷题][LeetCode]总 用动画的形式呈现解LeetCode题目的思路 参考链接 000-十大经典排序算法

随机推荐

  1. 利用AWVS扫描Web漏洞

    实验目的 利用AWVS扫描Web漏洞. 实验原理 AWVS是一款知名的网络漏洞扫描工具,它通过网络爬虫测试你的网站安全,检测流行安全漏洞. 实验内容 AWVS是一个自动化的web应用程序安全测试工具, ...

  2. 单网口RFC2544测试——信而泰网络测试仪实操

    一.测试拓扑 拓扑说明 测试仪一个端口和DUT一个端口相连 DUT假设是一台交换设备,它能够把测试仪发送的流量直接转发回来 注意:要求DUT必须能够把收到的流量环回出来,否则没有办法测试 二.测试思路 ...

  3. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  4. 案例一:shell脚本指定日期减去一天

    如果只减去一天的话,直接写就可以了. #date -d"yesterday 20150401" +%Y%m%d 如果要减去几天,还可以这样写,如果用负数是往前数, #date -d ...

  5. CDH5.16.2离线安装(详细)

    目录 01 Coudera Manager 02 环境准备 03 CM安装 01 Coudera Manager 概念:拥有集群自动化安装.中心化管理.集群监控.报警功能的一个工具,使集群安装从几天时 ...

  6. 『无为则无心』Python日志 — 67、logging日志模块处理流程

    目录 1.概括理解 2.详细说明 3.应用示例 1.概括理解 了解了四大组件的基本定义之后,我们通过图示的方式来理解下信息的传递过程: 也就是获取的日志信息,进入到Logger日志器中,传递给处理器确 ...

  7. 假如让你来设计SSL/TLS协议,你要怎么设计呢?

    摘要:本文将从设计者的视角介绍如何一步步设计出一个简易版的 SSL/TLS 的过程,在文章的最后,再简单介绍 TLS 1.2 版本的工作机制,以此帮助大家对 SSL/TLS 协议的基本原理有一个更深入 ...

  8. JAVA String、StringBuilder、和StringBuffer的区别,及如何使用

    目录 String类 一.String类的理解和创建对象 二.String类创建的方式 两种创建String对象的区别 测试题 三.String常用方法 四.StringBuffer类 1.Strin ...

  9. Redis-基本概念、java操作redis、springboot整合redis,分布式缓存,分布式session管理等

    NoSQL的引言 Redis数据库相关指令 Redis持久化相关机制 SpringBoot操作Redis Redis分布式缓存实现 Resis中主从复制架构和哨兵机制 Redis集群搭建 Redis实 ...

  10. ASP.NET Core 6框架揭秘实例演示[18]:HttpClient处理管道

    我们知道ASP.NET的核心就是由中间件组成的请求处理管道,HttpClient也采用了类似的设计.HttpClient管道由一组HttpMessageHandler对象构成,这些HttpMessag ...