八大排序算法

  • 八大排序分类
  • 01 冒泡排序
  • 02 直接插入排序
  • 03 简单选择排序
  • 04 希尔排序
  • 05 快速排序
  • 06 堆排序
  • 07 归并排序
  • 08 桶排序/基数排序
  • 参考资料

八大排序分类

​ 笔者是在LeetCode刷题过程中来复习排序算法的,虽然在刷题中我们可以使用高级编程语言提供封装好的排序算法便捷实现排序任务,例如 C++ 里可以通过 STL 中基于快速排序实现的 std::sort() 算法便捷实现排序任务。而且刷题时除非题目考点就是排序算法,否则也不建议自己手写排序算法。

​ 但是知识都是有着累积发展的过程,我们熟习各种基础的排序算法可以加深自己对算法的基本理解;同时我们也可以基于对基础排序算法的理解,快速解出由这些排序算法引申出来的题目。

​ 八大排序算法有着不同的实现思想,他们实现的时间复杂度也有所差异,根据不同的时间复杂度和算法稳定性他们有着不同的应用场景:

  • 快速排序、堆排序和归并排序具有较好的算法效率,其中快速排序性能最好但是算法不稳定,堆排序不需要额外的空间开销,而归并排序是稳定性较高的算法但是需要较大空间开销。
  • 在排序数据量较小的情况下,可以根据元素分布是否有规律选择使用直接插入排序或者简单选择排序,他们也能够提供稳定且较高的算法性能。一般不使用冒泡排序,其性能要逊色于其他算法。
  • 桶排序/基数排序是一种稳定的算法且具有较好算法性能,但是该算法的使用场景存在一定局限性,需要根据排序任务和待排序元素的属性和特征来使用。

​ 分别根据算法平均时间复杂度和算法实现难度划分八大排序算法如下图:

​ 开始详细介绍排序算法之前,我想向你强烈安利一个学习数据结构与算法的神奇网站 VisuAlgo http://visualgo.net。这个网站里面有各种数据结构和算法的动画展示,在教师教授或者学生自行学习相关数据结构和算法时可以十分直观的呈现算法执行过程。

一个神奇的数据结构和算法学习网站:VisuAlgo

01 冒泡排序

算法思想:

​ 冒泡排序是一种简单的比较排序算法。在待排序的数组中,使用双层循环遍历数组,外循环保证遍历每个元素,内循环进行比较和交换。每一遍内循环过程中,对相邻的两个数依次进行比较和交换,让较大的数往下沉(向右移动),较小的往上冒(向左移动)。

​ 内循环不交换直接结束:如果内循环完全不交换,这意味着数组已经排序完成,我们可以在这个点上停止冒泡排序,这样可以提高算法效率。

执行样例:

输入:[29,10,14,37,14,25,10]

算法实现:

void bubbleSort(vector<int> &nums, int n){for(int i=0;i<n;++i){bool swapped = false;for(int j=1;j<numsLen-i+1;++j){if(nums[j]<nums[j-1]){swap(nums[j],nums[j-1]);swapped = true;}}if(!swapped){break;}}
}

02 直接插入排序

算法思想:

​ 直接插入排序的主要思想就是将序列视为有序和无序两个部分,将无序部分中的元素插入到有序部分的适当位置保证仍然有序。

​ 可以采用双层循环实现插入排序,外层循环扩展有序区间大小,内层循环用于将待排序元素插入到有序部分。

执行样例:

输入:[29,10,14,37,14,25,10]

算法实现:

void insertionSort(vector<int> &nums, int n) {for (int i = 0; i < n; ++i) {for (int j = i; j > 0 && nums[j] < nums[j-1]; --j) {swap(nums[j], nums[j-1]);}}
}

03 简单选择排序

算法思想:

​ 简单选择排序的核心思想就是对号入座,第一步将待排序序列中最小的元素放在数组第一个位置,以此类推始终找无序部分中最小的元素放到有序部分的末尾。

​ 可以采用双层循环实现简单选择排序:外层循环遍历每一个待放入正确元素的位置;内层循环用于选择无序部分中最小的元素,并将该元素与外层循环位置元素交换。

执行样例:

输入:[29,10,14,37,14,25,10]

算法实现:

void selectSort(vector<int> &nums, int n){for(int i=0;i<n-1;++i){int min = i;for(int j=i+1;j<n;++j){if(nums[min]>nums[j]){min = j;}}swap(nums[i],nums[min]);}
}

04 希尔排序

算法思想:

​ 希尔排序是对直接插入排序的改进,又被称为缩小增量排序。该算法的核心思想是多轮分割,分别插入,将数组分割成若干个子集,然后在子集内分别使用直接插入排序;然后迭代缩小增量即分割的步长,并重复插入排序过程,直到最终数组排序完成。

​ 实现过程中,首先以步长为 gap = length/2 把数组分割成若干个子集,然后在每个子集中进行插入排序。完成一轮分割和排序后,迭代缩小分割步长并根据缩小的步长进行下一轮分割和插入排序。直到步长缩小为 gap = 1时,则数组排序完成。

执行样例:

输入:[29,10,14,37,14,25,10]

步长 分割 排序
gap = 7 / 2 = 3 [29,10,14,37,14,25,10] [10,10,14,29,14,25,37]
gap = 3 / 2 = 1 [10,10,14,29,14,25,37] [10,10,14,14,25,29,37]

算法实现:

void shellSort(vector<int> &nums){int length = nums.size();int tmp;//步长int gap = length / 2;while (gap > 0) {for (int i = gap; i < length; i++) {tmp = nums[i];int preIndex = i - gap;while (preIndex >= 0 && nums[preIndex] > tmp) {nums[preIndex + gap] = nums[preIndex];preIndex -= gap;}nums[preIndex + gap] = tmp;}gap /= 2;}
}

05 快速排序

算法思想:

​ 快速排序是对冒泡排序的一种改进,其核心算法思想是:使用基准将要排序的数据分割成小于基准和大于基准的两部分;然后在被分割的两个部分中递归按基准划分的步骤,最终递归到一个部分仅有一个元素组成,此时数组排序完成。

​ 算法实现过程中,在使用快排之前可以将数组打乱,因为快排是不稳定的算法,在原数组大部分元素是有序的情况下效率提升不明显。

​ 实现快排的碰撞指针方法步骤如下:

  • 先选取基准,可以以序列第一个元素作为基准
  • 然后使用碰撞指针遍历数组,从指向数组尾部的指针 tail 开始移动,将数组尾部小于基准的元素覆盖到指向数组头部的指针 head;接着移动 head 找到大于基准的元素覆盖到 tail;重复该过程直到一次遍历完成,将基准值放到指针相遇位置,将数组划分为小于基准和大于基准的两部分。
  • 在被分割的两部分中递归选择基准和划分序列的步骤,直到递归结束完成排序

实现快排的快慢指针方法步骤如下:

  • 选取基准值,定义两个指针,cur 表示当前指针指向,pre 默认表示cur 指针的前一位;
  • cur 和 pre 指针依次进行递增比较,当 cur 发现大于基准值时,pre 暂停递增(即[pre+1] 指向了一个大于基准值的值);
  • cur 接着进行递增比较,当发现比基准值小时,对数组[pre+1] 和 数组[cur] 的值进行交换;
    不停的重复第2~3步操作,直到一轮循环结束(即 cur 从当前数据源从左移到了右);
  • 内循环结束后,将数组[pre+1] 的值与基准值进行交换;以基准值为分割点,将本次数据源分为两个小的数组,依次从第1步递归循环操作;

执行样例:

输入:[29,10,37,14,25,10,14]

​ 为了友好展示快排的效果我们稍微调整了一下之前的例子。另外更值得注意的是,受限于VisuAlgo网站动态演示创建方式,动画仅展示使用快慢指针对划分的部分进行分割的方法。

算法实现:

// 碰撞指针实现
void quickSort(vector<int> &nums, int l, int r) {if (l + 1 >= r) {return;}int head = l, tail = r - 1, key = nums[head];while (head < tail){while(head < tail && nums[tail] >= key) {--tail;}nums[head] = nums[tail];while (head < tail && nums[head] <= key) {++head;}nums[tail] = nums[head];}nums[head] = key;quick_sort(nums, l, head);quick_sort(nums, head + 1, r);
}// 快慢指针实现
void quickSort(vector<int> &nums, int l, int r) {if (l + 1 >= r) {return;}int cur = l;int pre = cur - 1;int key = nums[r-1];while (cur < r) {while (nums[cur] < key && ++pre != cur) {swap(nums[cur], nums[pre]);}cur++;}   swap(nums[++pre], nums[r-1]);quickSort(nums, l, pre);quickSort(nums, pre + 1, r);
}

06 堆排序

算法思想:

​ 堆排序是对简单选择排序的改进,其核心思想是利用大根堆或者小根堆的树形结构,不断获取堆顶元素存入排序序列。大根堆是指一棵二叉树的每个节点值都大于或者等于它的左右子节点值,其根节点为最大值;小根堆反之。

​ 实现堆排序由两个关键任务:一是要构建大根堆或者小根堆;二是在取出堆顶之后,调整堆保持大根堆或者小根堆的树形结构。

堆构建过程:堆构建有自上而下和自下而上两种方法,我们采用简单的自上而下构建

  • 根据数组顺序插入树节点
  • 如果插入节点值小于父节点,继续插入其他节点
  • 如果插入节点值大于父节点,那么需要将该节点不断上浮,直到找到合适的插入位置
  • 需要注意的是大根堆或者小根堆都是一棵完全二叉树,可以直接使用数组映射完全二叉树,不需要去另外构建树节点结构体;使用数组映射完全二叉树,索引从 0 开始具有如下性质:
    • 当前节点的父节点索引为(i-1)/2
    • 当前节点的左节点索引为2*i+1
    • 当前节点的左节点索引为2*i+2 = 2*(i+1)

堆调整过程

  • 取出堆顶之后,用最后一个叶子节点与堆顶交换
  • 因为最后一个叶子节点是数组的最小元素,所以将它放到大根堆堆顶,就破坏大根堆堆的树形结构,需要调整堆
  • 选取根节点的左右节点中较大的节点与当前根节点交换
  • 交换后如果破坏了子树的堆结构,就需要按照上述调整步骤递归地调整堆结构

执行样例:

输入:[29,10,14,37,16,25,20]

为了友好展示快排的效果我们稍微调整了一下之前的例子,删去了重复的元素

  1. 大根堆构建过程

  1. 堆排序过程

算法实现:

// 下沉调整大根堆,比较节点及其子节点,节点值大的子节点与父节点交换
void sink(vector<int> &nums, int i,int heapSize){if(heapSize==0 || nums.empty()) return;// 找到当前节点、左节点、右节点中较大的一个节点索引int bigger = i;int leftChild = 2*i+1;if(leftChild < heapSize){bigger = nums[leftChild] > nums[i] ? leftChild:i;}int rightChild = 2*i+2;if(rightChild < heapSize){bigger = nums[rightChild] > nums[bigger] ? rightChild:bigger;}// 如果较大节点是左右节点中的一个,交换当前节点和较大节点if(bigger!=i){swap(nums[i],nums[bigger]);sink(nums,bigger,heapSize); // 递归调整}
}void buildHeap(vector<int> &nums, int heapSize){// 从最后一个非叶子节点开始构造int i=(heapSize-1)/2;for(i;i>=0;--i){sink(nums,i,heapSize);}
}// 递归排序时,堆根节点先与最后一个节点进行交换,交换后,堆大小减1,并对根节点进行下沉调整
void sort(vector<int> &nums, int &heapSize){swap(nums[0], nums[heapSize - 1]);--heapSize;sink(nums,0,heapSize);
}void heapSort(vector<int> &nums){int heapSize = nums.size();buildHeap(nums,heapSize);for(int i=0;i<nums.size()-1;++i){sort(nums,heapSize);}
}

07 归并排序

算法思想:

​ 归并排序的核心思想是采用分治策略,将整个数组的排序任务分类为两个子问题,前一半排序和后一半排序,然后整合两个有序部分完成整体排序。即把数组分为若干个子序列,直到单个元素组成一个序列,然后将各阶段得到的序列组合在一起得到最终完整排序序列。

​ 归并排序任务可以如下分治完成:

  1. 把前一半排序
  2. 把后一半排序
  3. 把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成。

执行样例:

输入:[29,10,14,37,14,25,10]

算法实现:

// 将数组 a 的局部 a[s,m] 和 a[m+1,e] 合并到 tmp, 并保证 tmp 有序,然后再拷贝回 a[s,m]
void merge(vector<int>& arr, int start, int mid, int end, vector<int> tmp){int pTmp = 0;int pLeft = start; int pRight = mid+1;while(pLeft<=mid&&pRight<=end){if(arr[pLeft] < arr[pRight]){tmp[pTmp++] = arr[pLeft++];}else{tmp[pTmp++] = arr[pRight++];}}while(pLeft<=mid){tmp[pTmp++] = arr[pLeft++];}while (pRight<=end){tmp[pTmp++] = arr[pRight++];}for(int i=0;i<pTmp;i++){arr[start+i] = tmp[i];}
}// 归并排序递归调用,先排前半部分,在排后半部分,最后将两部分结果合并
void mergeSort(vector<int>& arr, int start, int end, vector<int> tmp){if(start < end){int mid = start + (end-start)/2;mergeSort(arr,start,mid,tmp);mergeSort(arr,mid+1,end,tmp);merge(arr,start,mid,end,tmp);}
}

08 桶排序/基数排序

算法思想:

​ 通排序,顾名思义就是为一个值设立一个桶,在通内记录每个值的属性,然后对桶进行排序。例如[25,10,14,14,14,25,10],我们遍历一遍数组可以建立三个桶[25,10,14],并将相同值的元素放到同一个桶中形成[[25,25],[10,10],[14,14,14]];然后对三个桶进行排序[10,14,25],然后依次输出桶中的元素完成排序。

​ 基数排序就是进行多次桶排序,基数排序中根据进制位数字分配桶,然后根据桶的顺序收集,接着在高进制位继续迭代该过程直到最高进制位完成排序。当然,基数排序也可以根据其他属性用于其他类型的排序,核心思想都是先按低优先级分配收集排序,再按高优先级分配收集排序。

执行样例:

输入:[8,27,19,15,30,6,9]

算法实现:

void radixSort(vector<int> &nums){// 计算最大位数int maxOne = *max_element(nums.begin(),nums.end());int bit = 1;while(maxOne>=10){maxOne /= 10;++bit;}// 创建十个桶vector<queue<int>> buckets(10);// 多次桶排序for(int m=0;m<bit;++m){// 分配 一次遍历将根据对应位的数值放到对应桶中for(int i=0;i<nums.size();++i){int tmp = nums[i];for(int j=0;j<m;++j){tmp/=10;}buckets[tmp%10].push(nums[i]);}// 情况原数组内容nums.clear();// 收集 根据桶的顺序收集桶中的元素for(int i=0;i<10;++i){while(!buckets[i].empty()){nums.push_back(buckets[i].front());buckets[i].pop();}}}}

参考资料

算法总结:这是一份全面&详细的排序算法学习指南

八大排序算法

C++堆排序的实现(超详细)

算法分析与设计 八大排序算法相关推荐

  1. 【算法分析与设计】排序算法的时间复杂度与O(NlogN)

    Q:基于比较的内排序算法能不能比 O ( N log ⁡ N ) O(N\log{N}) O(NlogN)

  2. 算法 经典的八大排序算法详解和代码实现

    算法 经典的八大排序算法详解和代码实现 排序算法的介绍 排序的分类 算法的时间复杂度 时间频度 示例 图表理解时间复杂度的特点 时间复杂度 常见的时间复杂度 空间复杂度 排序算法的时间复杂度 冒泡排序 ...

  3. python 排序算法 简书_Python---简析八大排序算法

    前言 1 .排序的概念 排序是计算机内经常进行的一种操作,其目的是将一组"无序"的记录序列调整为"有序"的记录序列. 排序分为内部排序和外部排序. 若整个排序过 ...

  4. 八大排序算法的 Python 实现

    八大排序算法的 Python 实现 本文用Python实现了插入排序.希尔排序.冒泡排序.快速排序.直接选择排序.堆排序.归并排序.基数排序. 1.插入排序 描述 插入排序的基本操作就是将一个数据插入 ...

  5. 八大排序算法图文讲解

    排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存. 常见的内部排序算法有:插入排序.希尔排序. ...

  6. C语言八大排序算法,附动图和详细代码解释!

    文章来源:电子工程专辑.C语言与程序设计.竹雨听闲 一.前言 如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功. 想写出精炼.优秀的代码,不通过不断的锤炼,是很难做到的. 二. ...

  7. 硬核!C语言八大排序算法,附动图和详细代码解释!

    来源 :C语言与程序设计.竹雨听闲等 一 前言 如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功. 想写出精炼.优秀的代码,不通过不断的锤炼,是很难做到的. 二 八大排序算法 ...

  8. 用python排序算法_Python - 八大排序算法

    1.序言 本文使用Python实现了一些常用的排序方法.文章结构如下: 1.直接插入排序 2.希尔排序 3.冒泡排序 4.快速排序 5.简单选择排序 6.堆排序 7.归并排序 8.基数排序 上述所有的 ...

  9. 序列划分c语言,一篇“get”C语言八大排序算法

    如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功. 想写出精炼.优秀的代码,不通过不断的锤炼,是很难做到的. 二.八大排序算法 排序算法作为数据结构的重要部分,系统地学习一下是 ...

  10. Python实现八大排序算法(转载)+ 桶排序(原创)

    插入排序 核心思想 代码实现 希尔排序 核心思想 代码实现 冒泡排序 核心思想 代码实现 快速排序 核心思想 代码实现 直接选择排序 核心思想 代码实现 堆排序 核心思想 代码实现 归并排序 核心思想 ...

最新文章

  1. 【机器学习实战】第3章 决策树(Decision Tree)
  2. (4)打鸡儿教你Vue.js
  3. 东南亚电商成长秘籍,教你从0到1把lazada店铺做起来
  4. PMcaff-活动| 产品经理免费培训最后一批通过名单公布啦!
  5. c# mongodb or查询_C# 查询MongoDB中的数据
  6. 聚集索引和非聚集索引(整理)
  7. php清空dns缓存文件,dns清空-windows刷新本地DNS缓存的几种方法
  8. python程序移植到linux,如何使python或perl脚本可移植到Linux和Windows?
  9. 获“CAIS紫金奖”,腾讯民汉翻译践行“科技向善”
  10. Vue.js的虚拟dom
  11. webstorm中文乱码问题
  12. 【毕设】知网文献检索列表中的 href 解析为可访问的 URL
  13. 外贸中一些单词的缩写
  14. 来自一枚初生牛犊不怕虎的小菜鸟的Mock.js使用,不足之处欢迎读者的指出 谢谢...
  15. tmp ubuntu 自动删除吗_如何清理/tmp?
  16. excel单元格数据有效性自定义
  17. 网络 DMZ 区和网络安全等级简介
  18. 从电子印章到印控一体化,企业印章管理更安全高效
  19. 如何优雅的写C++代码 Obotcha介绍(字符串转uint8_t)
  20. 一个简单的字幕滚动程序~~

热门文章

  1. 浏览器cookie数量与大小限制
  2. 【电子元件】稳压(齐纳)管 Zener Diode
  3. select下拉框option默认选中(php模板渲染)
  4. 8uftp,8uftp使用教程图解
  5. 内含干货PPT下载|一站式数据管理 DMS 关键技术解读
  6. linux下svn命令使用大全
  7. Spark安装和编程实践(Spark2.4.0)
  8. docker视频教程下载
  9. 大学四年,电脑必备的三个宝藏工具软件
  10. 利用nssm将jar包安装为windows服务