前言
高铁上实在闲的没事干,所以就把这篇本来打算鸽掉的来开个头,咕咕咕~
排序算法的重要性不言而喻,开玩笑,连你瓜程序设计考试都大概率考到(doge);
建议先在1.0 十大经典排序算法 | 菜鸟教程 (runoob.com)上面对各种排序算法进行了解;
本篇Blog包含七种排序算法:
1.快速排序; 2.插入排序; 3.选择排序; 4.冒泡排序; 5.堆排序;
6.归并排序; 7.基数排序;
声明
本篇Blog的排序代码基于C++,使用部分C++特性;
对于需要C版本的,稍作修改就可以;
难度分析
入门级(学了程设怎么也得会):冒泡排序,选择排序,插入排序;
进阶级:快速排序;
高阶级(涉及更复杂的算法思想):堆排序,归并排序,基数排序;
注:以上均为本人主观判断,不代表客观实际;
程序预处理部分
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
using namespace std;
常用函数
其实就是一个swap();
用于替代常用的
template<class Type>{
Type a=x,b=y;
Type tmp;
tmp=a;a=b;b=tmp;
}
swap的好处是:不用担心交换变量精度的缺失,无需构造临时变量,不会增加空间复杂度;而且方便,不是吗?(doge)
在C语言中,swap定义如下(当然不只是int):
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
而在C++中,swap()包含在 std中,同时传入的参数不必是指针,直接是两个变量传入之后就可以交换数值(妙蛙!);
入门级:
冒泡排序
算法说明
原理:
把一个无序数组里面的所有元素想象成一个个水管中的气泡,一个数据越大,对应的气泡就越大;而众所周知,气泡在水里,是要上浮的;
那么,先找到最下面的气泡(对应第一个元素[0]),如果这个气泡比上面一个[1]更大,那么它就会把上面一个挤下;然后再比较[0]和[2],如果更大,就再把它挤下来......以此类推,直到比较完最后一对;
算法步骤:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。(此时这个元素触顶,不再参与排序,d)
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.0 原始版本
// 1.0
//bubble sort <unoptimized>
//avg:O(n^2) best:O(n) worst:O(n^2)
//space:O(1)
//method:In-place
//stable
template<class Type>
static void bubbleSortBasic(vector<Type>& arr) {
int size = arr.size() ;
for (int i = 0; i < size-1; ++i) {
for (int j = 0; j < size -1-i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
template<class Type>
static void BubbleSortBasic(vector<Type>& arr) {
bubbleSortBasic(arr);
}
1.1 优化版本
优化点:
原始版本的冒泡排序,在最外面一重循环时需要遍历所有元素,每个元素都要在第二重循环内对未触顶的元素进行一对对的比较;
而面对类似于整个数组,只有第一个元素的位置无序,后面的元素全部有序的极端情况,这种全部遍历的行为显然多余;
因此,通过设置一个Flag,判断第二重循环的某轮两两比较有没有发生交换,如果没有,则说明未触顶的所有元素也已经完全有序,没有待排元素,直接结束排序;
当然,这样的优化聊胜于无,毕竟只是优化掉了很多次比较大小,本来也不怎么费事,但是我们崩三玩家(划掉)学程设的,当然要凹分凹到底(划掉)尽可能优化程序;
// 1.1
//bubble sort <optimized>
//avg:O(n^2) best:O(n) worst:O(n^2)
//space:O(1)
//method:In-place
//stable
template<class Type>
static void bubbleSortOptimized(vector<Type>& arr) {
int size = arr.size() ;
for (int i = 0; i < size-1; ++i) {
bool isSwapped = false;
for (int j = 0; j < size -1- i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
isSwapped = true;
}
}
if (!isSwapped) break;
}
}
template<typename Type>
static void BubbleSortOptimized(vector<Type>& arr) {
bubbleSortOptimized(arr);
}
选择排序
算法说明
算法步骤:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
补充:
选择排序的效率很低,优化之后也不怎么样,实际应用没什么人用,毕竟不如用冒泡,一样简单;
2.0 原始版本
// 2.0
//selection sort <unoptimized>
//avg:O(n^2) best:O(n^2) worst:O(n^2)
//space:O(1)
//method:In-place
//unstable
template<class Type>
static void selectSortBasic(vector<Type>& arr) {
int size = arr.size();
for (int i = 0; i < size - 1; i++) {
int min = i;
for (int j = i + 1; j <size; j++) {
if (arr[j] < arr[min]) min = j;
}
swap(arr[i], arr[min]);
}
}
template<class Type>
static void SelectSortBasic(vector<Type>& arr) {
selectSortBasic(arr);
}
2.1 优化版本
优化点:
原始的选择排序,什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好;唯一的好处就是不占用额外的内存空间;
然鹅,这东西如果不大动的话,还是O(n²) 的时间复杂度;大动的话,它就不能叫选择排序,而是混血,或者深度优化之后改叫堆排序(doge);
所以,这里就给个简单的优化版本了,思路就是找最小值的同时找最大值;
// 2.1
//selection sort <optimized>
//avg:O(n^2) best:O(n^2) worst:O(n^2)
//space:O(1)
//method:In-place
//unstable
template<typename Type>
void selectSortOptimized(vector<Type> &arr){
int len = arr.size();
for (int left = 0, right = len - 1; left < right; left++, right--){
int min = left; // 记录最小值
int max = right; // 记录最大值
for (int index = left; index <= right; index++){
if (arr[index] < arr[min])
min = index;
if (arr[index] > arr[max])
max = index;
}
// 最小值交换
swap(arr[min], arr[left]);
// 此处是先排最小值的位置,所以得考虑最大值在最小位置的情况
if (left == max) max = min;
swap(arr[max], arr[right]);
}
}
template<class Type>
static void SelectSortOptimized(vector<Type>& arr) {
selectSortOptimized(arr);
}
插入排序
算法说明
原理:
扑克牌玩过吗,一样的;你手上原来一张牌也没有,然后你开始拿牌,等到你拿了两张牌之后,你有了大小的概念,开始区别牌的大小;
于是拿第三张的时候,你会分辨它是多大,插到合适的位置,后面抽到的牌也一样;
最后你手上的牌就是按顺序放的了,介就是打牌的智慧吗;
算法步骤:
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
3.0 原始版本
// 3.0
//insertion sort <unoptimized>
//avg:O(n^2) best:O(n) worst:O(n^2)
//space:O(1)
//method:In-place
//stable
template<class Type>
static void insertSortBasic(vector<Type>& arr) {
int len = arr.size();
for(int i=1;i<len;i++){
int key=arr[i];
int j=i-1;
while((j>=0) && (key<arr[j])){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=key;
}
}
template<class Type>
static void InsertSortBasic(vector<Type>& arr) {
insertSortBasic(arr);
}
3.1 优化版本
优化点:
要优化的话,当然是提高效率了;
原来你是一只手打牌,现在改用两只手,左右脑分别控制,分别排序;
这就是折半插入,或者叫二分插入,是二分法的实践哒;
// 3.1
//insertion sort <optimized> (Binary search)
//avg:O(n^2) best:O(n) worst:O(n^2)
//space:O(1)
//method:In-place
//stable
template<class Type>
static void insertSortOptimized(vector<Type>& arr) {
int size = arr.size();
for (int i = 0; i < size; i++) {
int low = 0;
int high = i;
while (low <= high) {
int mid = (low + high) >> 1;
if (arr[mid] >= arr[i]) high = mid - 1;
else low = mid + 1;
}
Type tmp = arr[i];
for (high = i; high > low; high--) {
arr[high] = arr[high - 1];
}
arr[low] = tmp;
}
}
template<typename Type>
static void InsertSortOptimized(vector<Type>& arr) {
insertSortOptimized(arr);
}
进阶级:
快速排序
算法说明
算法步骤:
从数列中挑出一个元素,称为 "基准"(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
补充:
快速排序,快速排序,顾名思义,特点就是快速,而且经过不同程度的优化,效率还能进一步提高。常用,并且对于大多数情况效率优秀;
然鹅,虽然把快排放在进阶里面,但别以为它就简单,写对一个快排的难度不小的;
4.0 原始版本
// 4.0
//quick sort <unoptimized>
//avg:O(nlogn) best:O(nlogn) worst:O(n^2)
//space:O(1)
//method:In-place
//unstable
template<class Type>
static void quickSortBasic(vector<Type>& arr, int low, int high){
if (low >= high) return;
Type pivot = arr[low];
int head = low - 1, tail = high + 1;
while (head < tail) {
do head++; while (arr[head] < pivot);
do tail--; while (arr[tail] > pivot);
if(head<tail) swap(arr[head], arr[tail]);
}
quickSortBasic(arr, low, tail );
quickSortBasic(arr, tail + 1, high);
}
template<class Type>
static void QuickSortBasic(vector<Type>& arr) {
quickSortBasic(arr, 0, arr.size() - 1);
}
4.1 优化版本
优化点:
快排优化的关键在于pivot(基准)的选择与分区数量;
原始版本里,pivot是每个分区最前面的元素,而在优化版本里,通过真随机在(low,high)中选取显然更优;
对于分区,原始程序是全程partition 过程使用一个索引值,增加索引值,增加同时处理的数据区域,可以在一定情况下提高算法效率;
使用两个索引的,称为双路快排;使用三个索引的,称为三路快排;
下面是较为常见的双路随机快排;
// 4.1
//quick sort <optimized> (2 ways & true random)
//avg:O(nlogn) best:O(nlogn) worst:O(n^2)
//space:O(1)
//method:In-place
//unstable
template<class Type>
static void quickSortOptimized(vector<Type>& arr, int low, int high) {
if (low >= high) return;
random_device seed;
mt19937 engine(seed());
uniform_int_distribution<> distrib(low, high);
Type pivot = arr[distrib(engine)];
int head = low - 1, tail = high + 1;
while (head < tail) {
do head++; while (arr[head] < pivot);
do tail--; while (arr[tail] > pivot);
if(head<tail) swap(arr[head], arr[tail]);
}
quickSortOptimized(arr, low, tail );
quickSortOptimized(arr, tail + 1, high);
}
template<typename Type>
static void QuickSortOptimized(vector<Type>& arr) {
quickSortOptimized(arr, 0, arr.size() - 1);
}
高阶级:
堆排序
算法说明
基础概念:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
堆是一个近似 完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。
索引计算:
父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
左孩子索引:2*i+1
右孩子索引:2*i+2
算法步骤:
首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
补充:
堆排序真挺难理解的,尤其是对于那些对于“堆”没有任何概念的初学者,建议上B站找个视频看看详细过程;
5.0 原始版本
// 5.0
//heap sort <unoptimized>
//avg:O(n^2) best:O(n) worst:O(n^2)
//space:O(1)
//method:In-place
//unstable
template<class Type>//维护大根堆
static void heapify(vector<Type>& arr, int size, int i) {//i为此时维护的父节点索引
int largest = i;
int lson = i * 2 + 1;
int rson = lson + 1;
if (lson < size && arr[largest] < arr[lson])
largest = lson;
if (rson < size && arr[largest] < arr[rson])
largest = rson;
if (largest != i) {
swap(arr[largest], arr[i]);
heapify(arr, size, largest);
}//当largest==i时结束递归
}
template<class Type>
static void heapSortBasic(vector<Type>& arr) {
int i;
int size = arr.size();
//构建大根堆(从最后一个元素的父节点开始)
for (i = size / 2 - 1; i >= 0; --i) {
heapify(arr, size, i);
}
for (i = size - 1; i > 0; --i) {
swap(arr[i], arr[0]);//固定每次维护完的大根堆第一个元素至末尾(即最大元素)
heapify(arr, i, 0);
}
}
template<class Type>
static void HeapSortBasic(vector<Type>& arr) {
heapSortBasic(arr);
}
5.1 优化版本
优化点:
感觉,原始版本就够好了?
暂时没什么优化的想法,所以代码木得;
归并排序
算法说明
归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
6.0 原始版本
似乎现在都默认双路了,所以原始代码不写了,木得;
6.1 优化版本
// 6.1
//merge sort <optimized>
//avg:O(nlogn) best:O(nlogn) worst:O(nlogn)
//space:O(n)
//method:OuT-place
//stable
template<class Type>
static void merge(vector<Type>& arr, int low, int mid, int high) {
vector<Type> tmp(high - low + 1);
int i = low, j = mid + 1, k = 0;//i为arr前一半的下标,j为arr后一半的下标,k为tmp的下标
while (i <= mid && j <= high) {
if (arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
while (i <= mid) {
tmp[k++] = arr[i++];
}
while (j <= high) {
tmp[k++] = arr[j++];
}
k = 0;//fresh k
//copy tmp
for (int i = low; i <= high; ++i) {
arr[i] = tmp[k++];
}
}
template<class Type>
static void mergeSortOptimized(vector<Type>& arr, int low, int high) {
if (low < high) {//low==high时结束递归
int mid = (low + high)/2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
template<class Type>
static void MergeSortOptimized(vector<Type>& arr) {
mergeSort(arr, 0, arr.size()- 1);
}