1755 字
9 分钟
实现常用排序算法

博客目前还没什么内容,水点经典的排序算法吧。

快速排序#

简介#

快速排序(Quick Sort)是非常常用的排序算法,平均时间复杂度为O(nlogn)O(n \log n),最坏复杂度O(n2)O(n^2),是一种不稳定排序。虽然有O(n2)O(n^2)的最坏复杂度,但是快排在实践中的效率通常优于其他算法,这也是c++中的std::sort主要使用快排进行排序的原因(std::sort是混合排序算法,并非单一的快排,有空会出一篇文章解析一下其源码)

快排是典型的分治算法,快排选取一个基准值将数组按大小分成两份,一份大于基准值,一份小于基准值,然后对这两份分别递归地进行排序。

在具体实现中,通常用双指针完成这个划分操作,维护两个指针i、j,1到i-1储存\leqpivot的数,j+1到n储存\geqpivot的数,然后让i往前,j往后,如果i、j指向的两个元素都不符合条件只需要让两个元素交换就行了,一直移动i、j指针直到两个指针相遇,即完成了划分,然后再递归地进行。

实现#

void quickSort(int *begin, int *end){ if(end - begin < 2) return; int pivot = *(begin + (end - begin) / 2); //取中间元素的值为基准值,也可以选取首元素或尾元素等。 int *i = begin, *j = end - 1; while(true){ while(*i < pivot) ++i; //i递增时无需判断i < j,因为选取的pivot在原数组中,而且j右边的元素都大于等于pivot。 while(pivot < *j) --j; //只要遇到pivot或者j右边的元素,i就会停下来。反之,j递减时同理。 if(!(i < j)){ //递归地排序 quickSort(begin, i); //1 ~ i-1 的元素都满足小于等于pivot quickSort(j+1, end); //j+1 ~ n 的元素都满足大于等于pivot return; } std::iter_swap(i, j); //相当于swap(*i, *j) ++i; --j; //交换后一定满足*i <= pivot,*j >= pivot,因此移动指针。 } }

归并排序#

简介#

归并排序排序(Merge Sort)与快速排序不同,它是一种稳定排序(Stable Sort),也就是说,对于数组中相等的元素,归并排序可以保证它们排序后相对位置不改变。由于我们用整数int的数组的排序作例子,这个特点并不能很好地表现出来,实际上在某些场景中这个性质很重要,因此c++特别地在std::sort之外提供了一个稳定排序算法std::stable_sort

归并排序也是分治算法,归并排序重复进行合并操作,不断地将两个排序好的数组合并成一个排序好的数组,最终完成整个数组的排序。归并排序有两种实现,一种是空间复杂度O(1)O(1)的原地(In-place)算法,另一种需要额外O(n)O(n)的空间。

不使用额外空间的归并排序需要用到一个叫做手摇算法(Block Swap Algorithm/内存反转算法/三重反转算法)的技巧来交换数组中的两块内存,这会引入大量的变量交换操作从而增加算法消耗的时间,甚至有可能使归并排序退化到O(n2)O(n^2),本质上是一种时间换空间的方法,这里不多做介绍。较为常用的还是非原地的常规写法,这里也用这种实现方法来演示。

非原地版本的归并排序的平均、最坏时间复杂度都是O(nlogn)O(n \log n),非常优秀,但是实践中往往比快速排序慢上不少。

实现#

void mergeSort(int *begin, int *end){ if(end - begin < 2) return; int *mid = begin + (end - begin) / 2; mergeSort(begin, mid); mergeSort(mid, end); //先将数组划分为两份,每份进行排序后再合并。 int *tmp = new int[end - begin]; //申请额外的空间来临时存放合并后的结果 int *i = begin, *j = mid, *k = tmp; while(i < mid && j < end) //只要两个部分都还有元素就一直比较,取较小的元素放进tmp中。 if(*j < *i) *(k++) = *(j++); //这样的写法相当于 {*k = *j; ++k; ++j;} else *(k++) = *(i++); while(i < mid) *(k++) = *(i++); //退出上面的while循环后,两部分应该一个为空,一个非空 while(j < end) *(k++) = *(j++); //将非空的那个全部放到tmp中 memcpy(begin, tmp, (end - begin) * sizeof(int)); //将tmp中的元素拷贝回原位。 delete tmp; //删除申请的空间,防止内存泄露 return; }

堆排序#

简介#

堆排序就是利用堆进行排序,如果对堆这种数据结构不熟悉可以看一看我的这篇文章。

堆排的思想很简单,就是在需要排序的数组上建立堆,然后执行pop操作,直到堆为空,因为每次pop出来的元素都是堆中的最大/最小值,所以可以保证出来的元素有序。堆排也是一种不稳定排序,时间复杂度O(nlogn)O(n \log n)。堆排序相比于先前两种排序的优势是它不需要递归,堆排序使用了更少的栈空间,防止因递归层数过多引发的低效(尤其是对于快排来说)。

实现#

只要实现了堆的代码,堆排就很简单了。

class Heap{ inline int& _get(int node); //_get不能暴露给用户,因为可能使值被修改导致堆性质被破环。 public: std::vector<int> tree; Heap(){}; Heap(std::vector<int>& vec); inline int size(); inline int top(); inline int get(int node); bool isValid(int node); void shiftUp(int node); void shiftDown(int node); void push(int val); void pop(); void remove(int node); void set(int node, int val); }; Heap::Heap(std::vector<int>& vec){ tree = vec; for(int i = size()>>1; i; --i){ shiftDown(i); } } inline int Heap::size(){ return tree.size(); } inline int Heap::top(){ if(size() == 0) throw "堆不能为空"; return get(1); } inline int Heap::get(int node){ if(!isValid(node)) throw "无效节点"; return tree[node-1]; } inline int& Heap::_get(int node){ if(!isValid(node)) throw "无效节点"; return tree[node-1]; } bool Heap::isValid(int node){ return node >= 1 && node <= size(); } void Heap::shiftUp(int node){ //传入要调整的节点编号 while(node > 1 && _get(node>>1) < _get(node)){ //如果不是根节点而且父节点小于自己就交换自己和父节点 std::swap(_get(node>>1), _get(node)); node >>= 1; } } void Heap::shiftDown(int node){ while((node<<1|1) <= size()){ //如果有两个孩子 int max_chd = _get(node<<1) > _get(node<<1|1) ? node<<1 : node<<1|1; //取较大的孩子 if(_get(max_chd) > _get(node)){ std::swap(_get(max_chd), _get(node)); node = max_chd; } else break; } if(node<<1 == size() && _get(node<<1) > _get(node)){ //最后可能出现只有一个子节点的情况,单独判断 std::swap(_get(node<<1), _get(node)); } } void Heap::push(int val){ tree.push_back(val); shiftUp(size()); } void Heap::pop(){ if(size() == 0) throw "堆不能为空"; if(size() == 1){ tree.pop_back(); return; } std::swap(_get(1), _get(size())); tree.pop_back(); shiftDown(1); } void Heap::remove(int node){ if(size() == 0) throw "堆不能为空"; while(node > 1){ //向上交换到根节点 std::swap(_get(node>>1), _get(node)); node >>= 1; } pop(); } void Heap::set(int node, int val){ if(val > _get(node)){ _get(node) = val; shiftUp(node); } if(val < _get(node)){ _get(node) = val; shiftDown(node); } } //以上的代码在简介里的那篇文章中有讲解 void heapSort(int *begin, int *end){ std::vector vec(begin, end); Heap hp(vec); for(int *p = begin; p != end; ++p){ *p = hp.top(); hp.pop(); } }
实现常用排序算法
https://cyrus28214.github.io/posts/implement-common-sorting-algorithms/
作者
Cyrus
发布于
2023-09-20
许可协议
CC BY-NC-SA 4.0