七种常见经典排序算法总结(C++)
最近想复习下C++,很久没怎么用了,毕业时的一些经典排序算法也忘差不多了,所以刚好一起再学习一遍。
除了冒泡、插入、选择这几个复杂度O(n^2)的基本排序算法,希尔、归并、快速、堆排序,多多少少还有些晦涩难懂,幸好又博客园大神dreamcatcher-cx都总结成了图解,一步步很详细,十分感谢。
而且就时间复杂度来说,这几种算法到底有什么区别呢,刚好做了下测试。
代码参考: http://yansu.org/2015/09/07/sort-algorithms.html
//: basic_sort #include <iostream>
#include <vector>
#include <ctime>
#include <string> using namespace std; // 获取函数名字的宏
#define GET_NAME(x) #x
// 生成随机数的宏
#define random(a,b) (rand()%(b-a+1)+a)
// 打印容器对象(vector)的宏
#define PRT(nums) { \
for(int i =; i<nums.size(); i++){ \
cout << nums[i] << " "; \
}\
} /*
冒泡排序
基本思想: 对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序
图解: http://www.cnblogs.com/chengxiao/p/6103002.html
考的最多的排序了吧。
1. 两层循环,最里面判断两个数的大小,大的换到后面(正序)
2. 内部循环一遍后,最大的数已经到最后面了
3. 下一次内部循环从0到倒数第二个数(最后一个数通过第一步循环比较已经最大了)
4. 依次循环下去
时间复杂度O(n^2),空间复杂度是O(n)
*/
void bubble_sort(vector<int> &nums)
{
for (int i = ; i < nums.size() - ; i++) { // i用来控制已经冒泡的数字个数
for (int j = ; j < nums.size() - i - ; j++) { // 从最左边遍历到最后一个没有浮动的数字
if (nums[j] > nums[j + ]) {
nums[j] += nums[j + ];
nums[j + ] = nums[j] - nums[j + ];
nums[j] -= nums[j + ];
}
}
}
} /*
插入排序
基本思想: 每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
图解: http://www.cnblogs.com/chengxiao/p/6103002.html
1. 两层循环,第一层i表示从左开始已经排好虚的部分
2. 第二层循环,将当前的数以及它前面的所有数两两比较,交换大的数到后面(正序)
3. 保证前面的数是排序好的,将新读取的数通过遍历前面排好序的部分并比较,插入到合适的位置
时间复杂度O(n^2),空间复杂度是O(n)
*/
void insert_sort(vector<int> &nums)
{
for (int i = ; i < nums.size(); i++) { // i表示从左开始已经排好序的部分
for (int j = i; j > ; j--) { // 从当前数字位置遍历到最左边的数字位置
if (nums[j] < nums[j - ]) {
int temp = nums[j];
nums[j] = nums[j - ];
nums[j - ] = temp;
}
}
}
} /*
选择排序
图解: http://www.cnblogs.com/chengxiao/p/6103002.html
基本思想: 每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素
1. 两层循环,第一层从左到右遍历,读取当前的数
2. min存放最小元素,初始化为当前数字
3. 内部循环遍历和比较当前数字后后面所有数字的大小,如果有更小的,替换min为更小数字的位置
4. 内部遍历之后检查min是否变化,如果变化,说明最小的数字不在之前初始化的min位置,交换使每次循环最小的元素被移动到最左边。
时间复杂度O(n^2),空间复杂度是O(n)
*/
void selection_sort(vector<int> &nums)
{
for (int i = ; i < nums.size(); i++) { // 从左到右遍历所有数字
int min = i; // 每一趟循环比较时,min用于存放较小元素的数组下标,这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
for (int j = i + ; j < nums.size(); j++) {
if (nums[j] < nums[min]) {
min = j;
}
}
if (min != i) { //进行交换,如果min发生变化,则进行交换
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
} /*
希尔排序
图解: http://www.cnblogs.com/chengxiao/p/6104371.html
基本思想: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
1. 最外层循环设置间隔(gap),按常规取gap=length/2,并以gap = gap/2的方式缩小增量
2. 第二个循环从gap位置向后遍历,读取当前元素
3. 第三个循环从当前元素所在分组的上一个元素开始(即减去gap的位置),通过递减gap向前遍历分组内的元素,其实就是比较分组内i和i-gap元素的大小,交换大的到后面
希尔排序的时间复杂度受步长的影响,不稳定。
*/
void shell_sort(vector<int> &nums)
{
for (int gap = int(nums.size()) >> ; gap > ; gap >>= ) { // 遍历gap
for (int i = gap; i < nums.size(); i++) { // 从第gap个元素向后遍历,逐个对其所在组进行直接插入排序操作
int j = i - gap; // j是这个分组内i元素的上一个元素
for (; j >= && nums[j] > nums[i]; j -= gap) { // 从i向前遍历这个分组内所有元素,把大的交换到后面
swap(nums[j + gap], nums[j]);
}
}
}
} // 合并两个有序序列
void merge_array(vector<int> &nums, int b, int m, int e, vector<int> &temp)
{
// cout << "b: " << b << " " << "m: " << m << " " << "e: " << e << endl;
int lb = b, rb = m, tb = b;
while (lb != m && rb != e)
if (nums[lb] < nums[rb])
temp[tb++] = nums[lb++];
else
temp[tb++] = nums[rb++]; while (lb < m)
temp[tb++] = nums[lb++]; while (rb < e)
temp[tb++] = nums[rb++]; for (int i = b;i < e; i++)
nums[i] = temp[i];
// cout << "temp: ";
// PRT(temp);
// cout << endl;
} //递归对序列拆分,从b(开始)到e(结束)的序列,取中间点(b + e) / 2拆分
void merge_sort_recur(vector<int> &nums, int b, int e, vector<int> &temp)
{
int m = (b + e) / ; // 取中间位置m
if (m != b) {
merge_sort_recur(nums, b, m, temp);
merge_sort_recur(nums, m, e, temp);
merge_array(nums, b, m, e, temp); // 开始(b)到中间(m) 和 中间(m)到结束(e) 两个序列传给合并函数
}
} /*
归并排序
图解: http://www.cnblogs.com/chengxiao/p/6194356.html
基本思想: 利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
1. 合并两个有序序列的函数,合并后结果存入临时的temp
2. 从中间分,一直递归分到最小序列,即每个序列只有一个元素,单位为1(一个元素肯定是有序的)
3. 然后两两比较合并成单位为2的n/2个子数组,在结果上继续两两合并
时间复杂度是O(nlogn),空间复杂度是O(n)。
*/
void merge_sort(vector<int> &nums){
vector<int> temp;
temp.insert(temp.begin(), nums.size(), ); // 定义和初始化temp用于保存合并的中间序列
merge_sort_recur(nums, , int(nums.size()), temp);
} // 将启始位置b作为基准,大于基准的数移动到右边,小于基准的数移动到左边
void quick_sort_recur(vector<int> &nums, int b, int e)
{
if (b < e - ) {
int lb = b, rb = e - ;
while (lb < rb) { // 遍历一遍,把大于基准的数移动到右边,小于基准的数移动到左边
while (nums[rb] >= nums[b] && lb < rb) //默认第一个数nums[b]作为基准
rb--;
while (nums[lb] <= nums[b] && lb < rb)
lb++;
swap(nums[lb], nums[rb]);
}
swap(nums[b], nums[lb]);
// cout << "nums: ";
// PRT(nums);
// cout << endl;
quick_sort_recur(nums, b, lb);
quick_sort_recur(nums, lb + , e);
}
} /*
快速排序
图解: http://www.cnblogs.com/chengxiao/p/6262208.html
基本思想: 快速排序也是利用分治法实现的一个排序算法。快速排序和归并排序不同,它不是一半一半的分子数组,而是选择一个基准数,把比这个数小的挪到左边,把比这个数大的移到右边。然后不断对左右两部分也执行相同步骤,直到整个数组有序。
1. 用一个基准数将数组分成两个子数组,取第一个数为基准
2. 将大于基准数的移到右边,小于的移到左边
3. 递归的对子数组重复执行1,2,直到整个数组有序
空间复杂度是O(n),时间复杂度不稳定。
*/
void quick_sort(vector<int> &nums){
quick_sort_recur(nums, , int(nums.size()));
} // 调整单个二叉树的根节点和左右子树的位置,构建大顶堆
// 在左右子树中挑出最大的和根节点比较,把最大的数放在根节点即可
void max_heapify(vector<int> &nums, int root, int end)
{
int curr = root; // 根结点
int child = curr * + ; // 左子树
while (child < end) {
if (child + < end && nums[child] < nums[child + ]) {
child++;
}
if (nums[curr] < nums[child]) {
int temp = nums[curr];
nums[curr] = nums[child];
nums[child] = temp;
curr = child;
child = * curr + ;
} else {
break;
}
}
} /*
堆排序
图解: http://www.cnblogs.com/chengxiao/p/6262208.html
基本思想: 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
堆的概念(i是一个二叉树的根节点位置,2i+1和2i+2分别是左右子树):
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
1. 由底(最后一个有叶子的根节点n/2-1)自上构建大顶堆
2. 根节点(0)和末尾交换,末尾变为最大
3. 对余下的0到n-1个数的根节点(0)二叉树进行大顶堆调整(调用max_heapify)(根节点(0)的叶子节点已经大于下面的所有数字了)
堆执行一次调整需要O(logn)的时间,在排序过程中需要遍历所有元素执行堆调整,所以最终时间复杂度是O(nlogn)。空间复杂度是O(n)。
*/
void heap_sort(vector<int> &nums)
{
int n = int(nums.size());
for (int i = n / - ; i >= ; i--) { // 构建大顶堆
max_heapify(nums, i, n);
} for (int i = n - ; i > ; i--) { // 排序, 将第一个节点和最后一个节点交换,确保最后一个节点最大
int temp = nums[i];
nums[i] = nums[];
nums[] = temp;
max_heapify(nums, , i); // 重新调整最顶部的根节点
}
} void func_excute(void(* func)(vector<int> &), vector<int> nums, string func_name){
clock_t start, finish;
start=clock();
(*func)(nums);
finish=clock();
// PRT(nums); // 打印每次的排序结果
cout << endl;
cout << func_name << "耗时:" << float(finish-start)/float(CLOCKS_PER_SEC)* << " (ms) "<< endl;
} int main() {
vector<int> b;
srand((unsigned)time(NULL));
for(int i=;i<;i++)
b.insert(b.end(), random(,));
cout << "数组长度: " << b.size() << "; ";
// PRT(b); // 打印随机数组
cout << endl; void (*pFun)(vector<int> &);
string func_name; pFun = bubble_sort;
func_name = GET_NAME(bubble_sort);
func_excute(pFun, b, func_name); pFun = insert_sort;
func_name = GET_NAME(insert_sort);
func_excute(pFun, b, func_name); pFun = selection_sort;
func_name = GET_NAME(selection_sort);
func_excute(pFun, b, func_name); pFun = shell_sort;
func_name = GET_NAME(shell_sort);
func_excute(pFun, b, func_name); pFun = merge_sort;
func_name = GET_NAME(merge_sort);
func_excute(pFun, b, func_name); pFun = quick_sort;
func_name = GET_NAME(quick_sort);
func_excute(pFun, b, func_name); pFun = heap_sort;
func_name = GET_NAME(heap_sort);
func_excute(pFun, b, func_name);
} ///:~
在数组很小的情况下,没有太大区别。但是较长数组,考的最多的冒泡排序就明显比较吃力了~
具体原因只能从时间复杂度上面来看,但为什么差这么多,我也不是完全明白~
运行结果,排序算法分别耗时:
数组长度: 5000; bubble_sort耗时:183.4 (ms) insert_sort耗时:106.525 (ms) selection_sort耗时:68.036 (ms) shell_sort耗时:1.096 (ms) merge_sort耗时:1.226 (ms) quick_sort耗时:1.398 (ms) heap_sort耗时:1.514 (ms)
Program ended with exit code: 0
七种常见经典排序算法总结(C++)的更多相关文章
- 七种常见经典排序算法总结(C++实现)
排序算法是非常常见也非常基础的算法,以至于大部分情况下它们都被集成到了语言的辅助库中.排序算法虽然已经可以很方便的使用,但是理解排序算法可以帮助我们找到解题的方向. 1. 冒泡排序 (Bubble S ...
- python3实现几种常见的排序算法
python3实现几种常见的排序算法 冒泡排序 冒泡排序是一种简单的排序算法.它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来.走访数列的工作是重复地进行直到没有再需要 ...
- java讲讲几种常见的排序算法(二)
java讲讲几种常见的排序算法(二) 目录 java讲讲几种常见的排序算法(一) java讲讲几种常见的排序算法(二) 堆排序 思路:构建一个小顶堆,小顶堆就是棵二叉树,他的左右孩子均大于他的根节点( ...
- java讲讲几种常见的排序算法
java讲讲几种常见的排序算法(一) 目录 java讲讲几种常见的排序算法(一) java讲讲几种常见的排序算法(二) 以数组array={6,3,20,8,15,1}为例 冒泡排序 思路:从第0个到 ...
- java几种常见的排序算法总结
/*************几种常见的排序算法总结***************************/ package paixu; public class PaiXu { final int ...
- Python全栈开发之5、几种常见的排序算法以及collections模块提供的数据结构
转载请注明出处http://www.cnblogs.com/Wxtrkbc/p/5492298.html 在面试中,经常会遇到一些考排序算法的题,在这里,我就简单了列举了几种最常见的排序算法供大家学习 ...
- 用php实现四种常见的排序算法
几种常见的排序 排序是一个程序员的基本功,对于初级phper,更是可以通过排序算法来锻炼自己的思维能力. 所谓排序,就是对一组数据,按照某个顺序排列的过程.下面就总结四种常用的php排序算法,分别是冒 ...
- Java几种常见的排序算法
一.所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作.排序算法,就是如何使得记录按照要求排列的方法.排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面. ...
- Java实现7种常见的排序算法
数据结构中的内部排序:不需要访问外存便能完成,是一个逐步扩大记录的有序序列长度的过程. 可以分为5类: 1.插入排序:直接插入排序,稳定排序,时间复杂度为O(n^2)非递减有序,设置r[0]为哨兵进行 ...
随机推荐
- [React-Native]入门(Hello World)
(1)需要一台Mac(OSX),这个是前提,建议还是入手一本啦. (2)在Mac上安装Xcode,建议Xcode 6.3以上版本 (3)安装node.js:https://nodejs.org/dow ...
- 12.MySQL必知必会之分组数据
本文将介绍如何分组数据,以便能汇总表内容的子集,这涉及两个新SELECT语句子句,分别是 GROUP BY 子句和HAVING子句. 1.1 创建分组 分组是在SELECT语句的GROUP BY子句中 ...
- [笔记] 基于nvidia/cuda的深度学习基础镜像构建流程
基于NVidia开源的nvidia/cuda image,构建适用于DeepLearning的基础image. 思路就是先把常用的东西都塞进去,再装某个框架就省事儿了. 为了体验重装系统的乐趣,所以采 ...
- c++之旅:模板库中的容器
容器 C++中的容器包括array, vector, list,map,set 数组 array不可变长,创建时其大小就固定了,array中可以存储各种数据类型包括对象,不过array是在栈上分配的, ...
- win7系统下查看端口的占用情况以及如何删除端口进程
经常在本地测试开发使用tomcat的时候容易报端口占用的情况,比如我要查看8080端口的使用情况 1.按如下操作,输入 cmd 回车 2.在doc窗口中输入命令 netstat -ano | f ...
- 20145316许心远《Java程序设计》第4周学习总结
20145316许心远<Java程序设计>第4周学习总结 教材学习内容总结 6.继承与多态 1.继承共同行为 * 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需 ...
- JavScript 日期格式化
JavScript 日期格式化 //日期格式化 function formatDate(date,fmt) { if(date == null || typeof (date) == undefine ...
- 20145103JAVA第二次实验报告
实验二 Java面向对象程序设计 实验内容 1.初步掌握单元测试和TDD 2.理解并掌握面向对象三要素:封装.继承.多态 3.初步掌握UML建模 4.熟悉S.O.L.I.D原则 5.了解设计模式 实验 ...
- 20135320赵瀚青LINUX内核分析第三周学习笔记
赵瀚青原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 概述 本周是学习的主要是构造 ...
- [CF960F]Pathwalks
题目大意:给你一张$n$个点$m$条边的带权有向图,可能有重边和自环.边会按照顺序给出.让你求出一条最长的路径,使得路径上的边满足边权和出现的时间严格递增.路径可以重复经过同一个点. 想办法把它转化成 ...