我的上一篇文章向大家介绍了排序算法中的冒泡排序、插入排序和选择排序。它们都是平均时间复杂度为 O(n^2) 的排序算法,同时还为大家讲解了什么是原地排序和什么是排序的稳定性。下图是这三种算法的比较,不熟悉的同学也可以点击我的上一篇文章回顾一下。

小R同学:[数据结构与算法系列]排序算法(一)​zhuanlan.zhihu.com

由于它们的时间复杂度都比较高,所以在处理大规模数据的时候这三种算法就不那么适用了。接下我将为大家介绍另外两种更高效的排序算法——归并排序和快速排序。

声明:在下面所有的算法讲解中,我们默认我们需要对待处理数组进行升序排序,即排序好的数组中,左边的元素要小于右边的元素。

1.分治思想(Divide and Conquer)

在计算机科学中,分治法是基于多项分支递归的一种重要的算法思想。从名字可以看出,“分治”也就是“分而治之”的意思,就是把一个复杂的问题分成两个或多个相同或类似的子问题,直到子问题可以简单直接地解决,原问题的解即为子问题的合并。

分治算法一般是用递归来实现的,具体的分治算法可以按照下面三个步骤来解决问题:

  1. 分解: 将原问题分解为若干个规模较小,相对独立,与原问题形式相同的子问题。
  2. 解决: 若子问题规模较小且易于解决时,则直接解。否则,递归地解决各子问题。
  3. 合并: 将各子问题的解合并为原问题的解。

2.算法介绍

Ⅰ.归并排序(Merge Sort)

该算法是利用分治思想解决问题的一个非常典型的应用,归并排序的基本思路就是先把数组一分为二,然后分别把左右数组排好序,再将排好序的左右两个数组合并成一个新的数组,最后整个数组就是有序的了。

运用递归法归并排序的主要步骤: 1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 3. 重复步骤3直到某一指针到达序列尾 将另一序列剩下的所有元素直接复制到合并序列尾

  • 归并算法图解如下:
  • 归并排序算法实现代码:
#include<iostream>
#include<vector>using namespace std;void Merge(vector<int>& , int , int , int );
void MergeSort(vector<int>& , int , int );int main() {vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };MergeSort(test,0,test.size()-1);for (auto x : test)cout << x << " ";return 0;
}void Merge(vector<int>& arr, int left, int mid, int right) {int i = left;int j = mid + 1;int k = 0;vector<int> temp(right - left + 1);while (i <= mid && j <= right)temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];while (i <= mid)temp[k++] = arr[i++];while (j <= right)temp[k++] = arr[j++];for (int m = 0; m < temp.size(); m++)arr[left + m] = temp[m];
}void MergeSort(vector<int>& arr,int left, int right) {if (left >= right) return;int mid = left + (right - left) / 2;MergeSort(arr, left, mid);MergeSort(arr, mid + 1, right);Merge(arr, left, mid, right);
}

代码分析:

归并排序中需要用到两个函数,一个是MergeSort函数,一个是Merge函数。MergeSort函数的作用是把数组中left至right的元素全部排列好。而Merge函数的作用是把左右两个已经排序好的数组合并成一个数组。

  • Merge函数的编写非常重要,我们需要创建一个新的数组temp,数组大小为right-left+1.然后定义两个下标ij, 其中i=left, j=mid+1,i表示第一个数组的起始位置,j表示第二个数组的起始位置。同时还需要一个下标k来标记temp数组中填入元素的位置。

开始遍历两个数组,比较ij所指元素的大小,将较小者放入temp数组中,同时脚标和k向后移动。当其中一个子数组循环完后,将剩下数组中的元素依次放入temp数组中。

最终,将temp数组拷贝回原数组array,返回的数组就是经过归并排序好的数组了。

  • MeergeSort函数主要是用于递归调用。当right>=left时,就直接return。否则,找到数组的中间下标,将数组一分为二,分别两边两边数组进行归并排序,最后将两个数组用Merge函数合并起来。

算法分析:

  • 归并排序的时间复杂度?

归并排序的递推公式为T(n)=2*T(n/2)+n

该递归式表明,对n个元素递归排序所需时间复杂度,等于左右子区间n/2个元素分别递归排序的时间,加上将两个已排好的子区间合并起来的时间O(n)

当递归循环至最后一层时,即n=1时,T(1)=1,于是可以推导出归并排序的时间复杂度为O(nlongn)

  • 归并排序是原地排序吗?

从原理中可以看出,在归并排序过程中我们需要分配临时数组temp,所以不是原地排序算法,空间复杂度为O(n).

  • 归并排序是稳定的排序算法吗?

当我们遇到左右数组中的元素相同时,我们可以先把左边的元素放入temp数组中,再放入右边数组的元素,这样就保证了相同元素的前后顺序不发生改变。所以,归并排序是一个稳定的排序算法。

Ⅱ.快速排序(Quicksort)

快速排序,也就是我们常说的“快排”。其实,快排也是利用的分治思想。它具体的做法是在数组中取一个基准pivotpivot位置可以随机选择(一般我们选择数组中的最后一个元素)。选择完pivot之后,将小于pivot的所有元素放在pivot左边,将大于pivot的所有元素放在右边。最终,pivot左侧元素都将小于右侧元素。接下来我们依次把左侧的子数组和右侧子数组进行快速排序。如果左右两侧的数组都是有序的话,那么我们的整个数组就处于有序的状态了。

快速排序的主要步骤为: 1. 挑选基准值:从数组中挑出一个元素,称为“基准”(pivot) 2. 分割:重新排序数组,所有比pivot小的元素摆放在pivot前面,所有比pivot值大的元素摆在pivot后面(与pivot值相等的数可以到任何一边)。 3. 递归排序子数组:递归地将小于pivot元素的子序列和大于pivot元素的子序列排序。 4. 递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。

  • 快速排序图解如下:
  • 快速排序实现代码
#include<iostream>
#include<vector>using namespace std;int partition(vector<int>& , int , int );
void QuickSort(vector<int>& , int , int );int main() {vector<int> test = { 3, 7, 6, 4, 5, 1, 2, 8 };QuickSort(test,0,test.size()-1);for (auto x : test)cout << x << " ";return 0;
}int partition(vector<int>& arr, int left, int right) {int pivot = right;int location = left;for (int i = left; i < right; i++) {if (arr[i] < arr[pivot]) {int temp = arr[i]; arr[i] = arr[location]; arr[location] = temp;location++;}}int temp = arr[pivot]; arr[pivot] = arr[location]; arr[location] = temp;return location;
}void QuickSort(vector<int>& arr,int left, int right) {if (left >= right) return;int pivot = partition(arr, left, right);QuickSort(arr, left, pivot-1);QuickSort(arr, pivot + 1, right);
}

代码分析

快速排序算法中有两个函数,QuickSort函数和partition函数。partition函数的作用返回pivot下标,意思是此时,所有在pivot左侧的元素都比pivot的值小,在右侧的值比pivot大。接下来对左右两侧的数组递归调用QuickSort函数进行快排。

我们每次指定pivot指向最后一个元素,同时定义一个变量location,用来标记pivot最后应该置于的位置。在location左侧的所有元素都是比pivot值小的,从location开始,右侧所有元素都比pivot大。

只要遍历到的元素比pivot的值小,就与location所指的元素进行交换,同时location加一,更新pivot应该在的位置。

数组遍历结束,最后元素pivotlocation所指元素进行交换,这样,pivot左侧的元素就全部比pivot小,右侧元素全部比pivot大了。

算法分析:

  • 快速排序的时间复杂度?

快排的时间复杂度也可以像归并排序那样用递推公式计算出来。如果每次分区都刚好把数组分成两个大小一样的区间,那么它的时间复杂度也为O(nlogn).但是如果遇到最坏情况下,该算法可能退化成O(n^2).

  • 快速排序是原地排序吗?

根据原理可以知道,快速排序没有额外的内存消耗,故是一种原地排序算法。

  • 快速排序是稳定的排序算法吗?

因为分区操作涉及元素之间的交换,例如下图,当遍历到第一个小于2的元素1时,会交换1与前面的3,因此两个相等3的顺序就发生了改变。所以快速排序不是一个稳定的排序算法。

3.总结回顾

本文介绍的两个排序算法归并排序和快速排序的平均时间复杂度都为O(nlogn),但是他们的思路有所不同。

归并排序:先排序左右子数组,然后合并两个子数组成有序数组。

快速排序:先通过pivot找出左右子数组,然后对左右子数组递归调用快速排序。

下图为本文总结

未完待续......

数据结构排序算法实验报告_[数据结构与算法系列]排序算法(二)相关推荐

  1. 数据结构排序算法实验报告_数据结构与算法-堆排序

    堆排序 堆排序是指利用堆这种数据结构所设计的一种排序算法.堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是小于(或者大于)它的父节点,堆排序的时间复杂度为O(nlogn).( ...

  2. python链表和树实验报告_数据结构树和森林实验报告

    _ 树和森林应用实验 实验报告 实验目的 ( 1) 掌握树和森林的二叉链表表示方法. (2) 掌握树和二叉树的结构及算法之间的对应关系. (3) 掌握树的两种遍历算法及其应用. 实验运行环境 Visu ...

  3. 机器学习线性回归算法实验报告_从零实现机器学习算法(九)线性回归

    1. 回归简介 在客观世界中普遍存在着变量与变量之间的关系.变量之间的关系一般可以分为确定关系和不确定关系.确定关系是指变量之间的关系可以通过函数关系来表达.非确定关系即所谓的相关关系.而回归分析是研 ...

  4. 数据结构三元组实验报告_数据结构实验报告

    实验一 约瑟夫问题 实验学时: 3 学时 实验类型:设计 实验要求:必修 一.实验目的 熟练掌握线性链表的基础知识: 能够使用 C++ 或其他程序设计语言编程实现线性链表: 能够使用线性链表构造正确而 ...

  5. 数据结构三元组实验报告_数据结构三元组项目报告

    数据结构项目报告 项目题目:三元组 项目成员: 日期:2012年4月1号 1. 题目与要求 1.1问题提出 详细叙述本项目所要实现的问题是创建一个三元组并且实现一些有关三元组的操作. 1.2 本项目涉 ...

  6. 动态规划算法实验报告_搞懂这几点,动态规划算法就是那么简单

    动态规划(Dynamic programming,简称DP),是大家都觉得比较难以掌握的算法.为了应付面试,我们经常会背诵一下斐波那楔数列或者背包问题的源码,其实,只要理解了思想,掌握基本的模型,然后 ...

  7. 动态规划算法实验报告_强化学习之动态规划算法

    如今的强化学习研究大体分为了两个研究学派:一个是以Sutton,Sliver等人为代表的value-based学派,他们主要从值函数近似角度入手去研究强化学习,这也是强化学习早期最初发展起来时沿用的路 ...

  8. 递归与分治java策略实验报告_递归与分治策略–计算机算法设计与分析

    递归概念:直接或者间接调用自身的算法,称为递归运算. 分治思想:把一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相等,递归解决子问题后再将结果合并 下方为一些应用函数.因为 ...

  9. 机器学习线性回归算法实验报告_机器学习笔记 线性回归

    一.线性回归找到最佳拟合直线 1. 定义 线性回归是通过现有数据,让训练模型生成一个拟合公式,从而计算目标数据的预测值. 在统计学中,线性回归(Linear Regression)是利用称为" ...

最新文章

  1. oracle sql 获取本季度所有月份,上季度所有月份
  2. 【数学建模】线性代数知识汇总,参加建模大赛的小伙伴看过来,它会是你的最优选
  3. 归纳推理测试没做完_朋友买了1斤紫菜,2年还没吃完,我教他这样做,2个月就吃完了...
  4. 网址发布页模板,带网址测速+域名检测功能
  5. python密码传参有特殊字符如何解决_无法在python selenium scrip中使用带有特殊字符“$”的密码...
  6. 交换排序图解_10大经典排序算法,20+张图就搞定
  7. 认证服务器的搭建_这个认证正式发布!
  8. Java设计文本编辑器
  9. 实用工具系列 - Xshell安装下载与使用
  10. 《Axure RP8网站和APP原型制作 从入门到精通》
  11. Android摇一摇实现
  12. PHP简单实现个人网站
  13. [redis]知识回顾之redis主从+哨兵搭建简要记录
  14. 理解数据仓库中星型模型和雪花模型
  15. 0 0 0 ’\0’ 区别
  16. 视频教程-AI 教程illustrator从入门到精通-Illustrator
  17. 为什么要有无参构造方法,无参构造的运行原理
  18. middles在python中什么意思_middles是什么意思_middles在线翻译_英语_读音_用法_例句_海词词典...
  19. 为什么你建出的模型不好看?
  20. 60行代码爬取知乎神回复

热门文章

  1. 01_数据库连接池,数据源,ResultSetMetaData,jdbc优化
  2. android 文件读取错误,Android源文件从SD卡读取错误问题,怎么处理
  3. code vs 集成tfs_关于编译器和集成开发环境,一文给你讲明白!
  4. 基于ZF网络的Faster RCNN网络结构详解(非常详细版)
  5. 全卷积网络FCN详解
  6. 牛客网NC112--进制转换
  7. 精通Spring Boot—— 第二十一篇:Spring Social OAuth 登录简介
  8. vjue 点击发送邮件如何处理
  9. C/C++通用Makefile
  10. JWT实现token-based会话管理