【数据结构】排序算法及优化整理
排序算法
- 排序算法
- 选择排序 Selection Sort
- 插入排序 Insertion Sort
- 归并算法 Merge Sort
- 快速排序 Quick Sort
- 堆排序 Heap Sort
- 二叉堆的基础操作
- 堆的初始化
- Shift Up 往最大堆中添加一个元素
- ShiftDown 从堆中取出一个元素
- 基础堆排序和Heapify
排序算法
选择排序 Selection Sort
思路:每次遍历找到剩余数组中最小的元素,并依次swap到数组开头
时间复杂度:O(n^2)
template <typename T> void selectionSort(int arr[], int n) {for( int i = 0; i < n; i ++ ) {// 寻找[i ... n-1]的最小值int minIndex = i;for( int j = i; j < n; j ++ ) if(arr[j] < arr[minIndex])minIndex = j;swap(arr[i], arr[minIndex]);} }
插入排序 Insertion Sort
- 思路:将选定元素排到前序合适的位置上(类似扑克理牌的思路
遍历到6时,6应该放在8的前面,于是将6和8调换位置;
下一个是2,首先2比8小,所以2和8调换位置;2也比6小,于是2和6调换位置。
以此类推。
时间复杂度:O(n^2)
template <typename T> ![在这里插入图片描述](https://img-blog.csdnimg.cn/6a494eb6a6e04cbfbaefe033ea1f817b.png)void insertionSort(T arr[], int n) {for( int i = 1; i < n; i ++ ) { // i = 1开始,因为i = 0时只有一个元素不需要排序// 寻找元素arr[i]合适的插入位置for( int j = i; j > 0; j -- ) { // j > 0 非 j >= 0,因为每次都将arr[j]和arr[j-1]比较// 避免j-1越界,所以j最小只能到1if( arr[j-1] <= arr[j] )break;else // arr[j-1] > arr[j]swap(arr[j], arr[j-1]);}// for( int j = 1; j > 0 && arr[j-1] > arr[j]; j -- )// swap(arr[j], arr[j-1]);} }
优化
当前的算法在每次循环中要swap很多次,减少赋值操作的次数。
当考察元素2时,先将2复制出一份;
考察2前一个元素8,因为8比2大所以2不应该放在当前位置,将8赋值2的位置;
再向前考察8的位置,2比当前位置前一个元素6要小,所以2也不应该在这个位置,将6赋值2的当前位置;
再向前考察6的位置,当前位置没有前一个元素,所以2只能放在当前位置。
swap操作是三次赋值,这样优化成一次赋值
template <typename T>
void insertionSort(T arr[], int n) {for( int i = 1; i < n; i ++ ) { // i = 1开始,因为i = 0时只有一个元素不需要排序// 寻找元素arr[i]合适的插入位置T e = arr[i];int j; // 保存元素e应该插入的位置for( j = i; j > 0; j -- ) { // j > 0 非 j >= 0,因为每次都将arr[j]和arr[j-1]比较// 避免j-1越界,所以j最小只能到1if( arr[j-1] > e )arr[j] = arr[j-1];else // arr[j-1] <= ebreak;}// for( int j = 1; j > 0 && arr[j-1] > e; j -- )// arr[j] = arr[j-1];arr[j] = e;}
}
在近乎有序的数组中,插入排序的性能要远远优于选择排序。
归并算法 Merge Sort
排序思路:
先将整个数组不断分割,然后向上归并排序。
时间复杂度:O(nlogn)
具体实现
#include <iostream> #include <algorithm>using namespace std;// 将arr[l ... mid]和arr[mid+1 ... r]两部分进行归并 template <typename T> void __merge( T arr[], int l, int mid, int r ) {T aux[r-l+1];for( int i = l; i <= r; i ++ )aux[i-l] = arr[i];int i = l, j = mid + 1;for( int k = l; k <= r; k ++ ) if( i > mid ) {arr[k] = aux[j-l];j ++;}else if( j > mid ) {arr[k] = aux[i-l];i ++;}else if( aux[i-l] < aux[j-l] ) {arr[k] = aux[i-l];i ++;}else {arr[k] = aux[j-l];j ++;} }// 递归使用归并排序,对arr[l ... r]的范围进行排序(重点:左闭右闭 template <typename T> void __mergeSort( T arr[], int l, int r ) {if( l >= r ) return;int mid = l + (r - l) / 2; // 防止溢出 __mergeSort(arr, l, mid);__mergeSort(arr, mid + 1, r);__merge(arr, l, mid, r); }template <typename T> void mergeSort( T arr[], int n ) {__mergeSort(arr, 0, n - 1); }
优化:
在输入数组近乎有序的情况下,归并排序会退化为O(n^2)
因为在这部分中
template <typename T> void __mergeSort( T arr[], int l, int r ) {if( l >= r ) return;int mid = l + (r - l) / 2; // 防止溢出 __mergeSort(arr, l, mid);__mergeSort(arr, mid + 1, r);、// 不管两部分排序情况如何,都会进行一遍merge__merge(arr, l, mid, r); }
如果左半边的最后一个元素比右半边的第一个元素还要小,说明数组已经是有序的了,不需要再进行merge操作,代码可优化如下:
template <typename T> void __mergeSort( T arr[], int l, int r ) {if( l >= r ) return;int mid = l + (r - l) / 2; // 防止溢出 __mergeSort(arr, l, mid);__mergeSort(arr, mid + 1, r);、if(arr[mid] > arr[mid+1])__merge(arr, l, mid, r); }
这样已经对近乎有序的数组有一定程度的优化,但归并排序无法优化到O(n)级别,但插入排序可以。
所以在数组近乎有序的时候最好使用插入排序。
在归并到最后数据量比较小的时候可以采用插入排序
因为此时数据近乎有序的可能性比较大。
template <typename T> void __mergeSort( T arr[], int l, int r ) { // if( l >= r ) // return;if( r - l <= 15 ) { // 取值并非最优,只是举例insertionSort(arr, l, r);return;}int mid = l + (r - l) / 2; // 防止溢出 __mergeSort(arr, l, mid);__mergeSort(arr, mid + 1, r);、if(arr[mid] > arr[mid+1])__merge(arr, l, mid, r); }
自底向上的归并排序
不需要递归只需要迭代
template <typename T> void mergeSortBU( T arr[], int n ) {for( int sz = 1; sz <= n; sz += sz ) for( int i = 0; i + sz < n; i += sz + sz ) // 对 arr[i ... i+sz-1] 和 arr[i+sz ... i+sz+sz-1] 进行归并__merge( arr, i, i + sz - 1, min(i + sz + sz - 1, n-1) ); // 注意数组越界问题 }
虽然说性能差不多,但是自顶向下要快一点。
但没有使用到数组的特性:用索引直接获取元素。所以这个算法可以在O(nlogn)下对链表之类的数据结构进行排序。
快速排序 Quick Sort
关键步骤 Partition
把每个数字都放在合适的位置
一般选取数组的第一个元素
时间复杂度:O(nlogn)
代码实现
// 对arr[l ... r]部分进行partition操作 // 返回p,使得arr[l ... p-1] < arr[p]; arr[p+1 ... r] > arr[p] template <typename T> int __partition(T arr[], int l, int r) {T v = arr[l];// arr[l+1 ... j] < v; arr[j+1 ... i-1] > vint j = l; // 是l不是l+1,因为要保证开始的arr[l+1 ... j]和arr[j+1 ... i-1]都是空for( int i = l + 1; i <= r; i ++ ) {if(arr[i] < v) {swap(arr[j+1], arr[i]);j++;// swap(arr[++j], arr[i]);}// 如果arr[i] >= v// 只需要进行i++即可}swap(arr[l], arr[j]);return j; }// 对arr[l ... r]部分进行快速排序 template <typename T> void __quickSort(T arr[], int l, int r) {if(l >= r)return;int p = __partition(arr, l, r);__quickSort(arr, l, p-1);__quickSort(arr, p+1, r); }template <typename T> void quickSort(T arr[], int n) {__quickSort(arr, 0, n-1); }
优化:
在最后数据量较少时也可以直接使用插入排序
// 对arr[l ... r]部分进行快速排序 template <typename T> void __quickSort(T arr[], int l, int r) {if(r - l <= 15) {insertionSort(arr, l, r);return;} int p = __partition(arr, l, r);__quickSort(arr, l, p-1);__quickSort(arr, p+1, r); }
优化力度不大,并不是主要优化方法
在近乎有序的数组中:
每次选择的标定元素所partition出的左右两边数组元素个数是极不平衡的,因为数组近乎有序,第一个元素往往是最小的。
循环往复,Quick Sort的时间复杂度会退化到O(n^2)。
解决方法:随机选取标定元素
// 对arr[l ... r]部分进行partition操作 // 返回p,使得arr[l ... p-1] < arr[p]; arr[p+1 ... r] > arr[p] template <typename T> int __partition(T arr[], int l, int r) {swap(arr[l], arr[rand()%(r - l + 1) + l]); // 随机选择标定元素!T v = arr[l];// arr[l+1 ... j] < v; arr[j+1 ... i-1] > vint j = l; // 是l不是l+1,因为要保证开始的arr[l+1 ... j]和arr[j+1 ... i-1]都是空for( int i = l + 1; i <= r; i ++ ) {if(arr[i] < v) {swap(arr[j+1], arr[i]);j++;// swap(arr[++j], arr[i]);}// 如果arr[i] >= v// 只需要进行i++即可}swap(arr[l], arr[j]);return j; }template <typename T> void quickSort(T arr[], int n) {srand(time(NULL)); // 随机种子__quickSort(arr, 0, n-1); }
经过优化后会好很多,但还是没有归并排序快。
两路快排
拥有大量重复键值元素的数组中,快排的性能很差
因为在之前的partition操作中,如果元素 = v,则会被分到 > v 的一组。当数组中有大量重复键值元素的时候,两端会极不平衡。
时间复杂度会退化到O(n^2)。
换一个思路:将 > v 的元素放在最右边
- 将 i 向后移直到遇到 >= v 的元素,然后将 j 向前移直到遇到 <= v 的元素。
然后交换 i 和 j 的位置
i 继续向后,j 继续向前,直到i和j重合。此时其实橙色部分表示的是 <= v 的元素,而紫色部分表示的是 >= v 的元素。
这个方法其实是将 == v 的元素分散到左右两边了,避免两端不平衡。
// 对arr[l ... r]部分进行partition操作 // 返回p,使得arr[l ... p-1] < arr[p]; arr[p+1 ... r] > arr[p] template <typename T> int __partition(T arr[], int l, int r) {swap(arr[l], arr[rand()%(r - l + 1) + l]); // 随机选择标定元素!T v = arr[l];// arr[l+1 ... i-1] <= v; arr[j+1 ... r] >= vint i = l+1, j = r;while(true) {while(i <= r && arr[i] < v) i ++;while(j >= l + 1 && arr[j] > v)j --;if(i > j) break;swap(arr[i], arr[j]);i ++;j --;}swap(arr[l], arr[j]);return j; }
三路快排
将数组分割成小于v,等于v和大于v。
当 arr[i] == v 时,只需要 i ++ 即可;
当 arr[i] < v 时,swap( arr[lt + 1], arr[i] ) ,然后 lt ++, i ++;
当 arr[i] > v 时,swap( arr[gt - 1], arr[i] ) ,然后 gt --;
这里 i 不加了,因为arr[gt - 1] 没被处理过,需要再次判断
最后 gt 和 i 重合表示处理结束。
swap(arr[l], arr[lt])
然后将小于v的部分和大于v的部分进行再次排序
**这样可以不用对大量的等于v的元素进行重复操作。**```C++
// 三路快速排序处理 arr[l ... r]
// 将arr[l ... r]分为 < v, == v, > v 三个部分
// 之后递归对 < v, > v 部分继续进行三路快速排序
template <typename T>
void __quickSort(T arr[], int l, int r) {if(r - l <= 15) {insertionSort(arr, l, r);return;} swap(arr[l], arr[rand()%(r - l + 1) + l]); // 随机选择标定元素!T v = arr[l];int lt = l; // arr[l+1 ... lt] < vint gt = r + 1; // arr[gt ... r] > v int i = l + 1; // arr[lt+1 ... i-1] == vwhile(i < gt) {if(arr[i] < v) {swap(arr[lt+1], arr[i]);lt ++;i ++;}else if(arr[i] > v) {swap(arr[gt-1], arr[i]);gt --;}else // arr[i] == vi ++;}swap(arr[l], arr[lt]);__quickSort(arr, l, lt-1);__quickSort(arr, gt, r);
}template <typename T>
void quickSort(T arr[], int n) {srand(time(NULL)); // 随机种子__quickSort(arr, 0, n-1);
}
```
堆排序 Heap Sort
优先队列 Priority Queue
普通队列:先进先出,后进后出
优先队列:出队顺序和入队顺序无关,和优先级相关
入队 出队 普通数组 O(1) O(n) 顺序数组 O(n) O(1) 堆 O(logn) O(logn)
二叉堆的基础操作
任何节点的值都不大于它的父节点(最大堆:树顶的元素是最大的值)
是一棵完全二叉树,用数组来存储整个堆
根节点从1开始标记
parent(i) = i/2
left_child(i) = 2*i
right_child(i) = 2*i + 1
堆的初始化
template<typename Item>
class MaxHeap {
private:Item* data;int count;
public:MaxHeap(int capacity) {data = new Item[capacity + 1]; // 因为索引是从1开始的count = 0;}~MaxHeap() {delete[] data;}int size() {return count;}bool isEmpty() {return count == 0;}
};
Shift Up 往最大堆中添加一个元素
添加元素到数组的尾部
但不满足堆的定义,需要将新加入的元素调整到一个合适的位置以满足堆的定义
和父节点进行比较,如果比父节点大则交换
在public中添加一个新的函数 insert
void insert(Item item) {assert(count + 1 <= capacity);data[count + 1] = item; // 隐藏数组越界的问题count++;ShiftUp(count);}
在private里添加ShiftUp函数,通过递归将新插入的元素调整到合适位置
- 步骤:判断当前元素与其parent的值的大小,如果当前元素比parent大,说明位置不合适,需要将其与parent互换,然后仍指向交换后的元素(已在parent的位置)。
void ShiftUp(int k) {while ( k > 1 && data[k / 2] < data[k]) { // 有索引就需要考虑索引越界swap(data[k / 2] < data[k]);k /= 2;}}
ShiftDown 从堆中取出一个元素
即只能取出根节点元素(最大值),然后将数组中的最后一个元素放到根节点中,相应的count --,来保证是一个完全二叉树。
但此时并不符合堆的定义,接下来就是调整元素位置来恢复成一个合理的最大堆(即将根节点一步一步向下挪到合适的位置 ——> ShiftDown)
- 步骤:如果子节点比当前节点大,则跟子节点中最大的那个交换位置,直到子节点的值都比当前节点小或当前节点是叶子节点。
在public里添加函数ExtractMax(根节点元素是最大值)
Item ExtractMax() {assert(count > 0); // 首先保证堆里有元素Item ret = data[1];swap(data[1], data[count]);count--; // 已经取出了一个元素,所以count --ShiftDown(1); // 将当前根节点向下移直到移到合适位置return ret;}
在private里添加函数ShiftDown
void ShiftDown(int k) {while (2 * k < count) { // 判断是否有孩子,只要有左孩子就是有孩子了int j = 2 * k; // j指向k的左孩子,j最终要指向需要和k交换的那个孩子if (j + 1 <= count && data[j + 1] > data[j]) // 如果有右孩子并且比左孩子大j += 1; // 那么j指向更大的那个孩子if (data[k] > data[j]) // 如果k比j所指的值还大,则说明k已经在合适的位置了break;swap(data[j], data[k]); // 否则k就应该和最大的孩子交换位置k = j; // 并更新k的值进行下一轮ShiftDown}}
基础堆排序和Heapify
- 实现一个基础的堆排序
template <typename T>
void heapSort1( T arr[], int n ) {Maxheap<T> maxheap = MaxHeap<T>(n);for( int i = n; i < n; i ++ )maxheap.insert(arr[i]);// 要从小到大排序的话,就需要反向把堆中元素赋值回arrfor( int i = n - 1; i >= 0; i -- )arr[i] = maxheap.extractMax();
}
这个实现中,先要遍历整个数组来建立堆(O(nlogn)),再要将所有的元素取出(O(nlogn)),可以进一步优化只需要遍历一遍(O(n))就建立成堆。
优化:Heapify
给定一个数组,将数组的排列变成堆的形状
所有的叶子节点可以看做成五个最大堆,而下标为 arr.size() / 2
的元素是第一个非叶子节点的节点,是第一个需要考察的节点。
第一个考虑 arr[5] = 22
,子节点的值比它大,不符合最大堆性质,直接进行ShiftDown
操作就可以,这棵子树就符合了最大堆性质。
下一个需要考虑的节点下标为4,选择子节点中值最大的那个来ShiftDown,那么这棵子树也满足了最大堆的性质。
下一个需要考虑的节点下标为3,选择子节点中最大的那个ShiftDown,那么这棵子树也满足了最大堆的性质。
下一个需要考虑的节点下标为2,选择子节点中最大的那个ShiftDown,arr[5] = 17 子节点为22,不符合最大堆性质,则继续ShiftDown直到满足性质。
最后一个需要考虑的是根节点 15 ,将与子节点ShiftDown到底,恢复整个最大堆的性质。
代码实现:
template<typename Item> class MaxHeap { private:Item* data;int count; public:MaxHeap(int capacity) {data = new Item[capacity + 1]; // 因为索引是从1开始的count = 0;}MaxHeap(Item arr[], int n) { // heapifydata = new Item[n+1];capacity = n;for(int i = 0; i < n; i ++) data[i+1] = arr[i];count = n;for( int i = count / 2; i >= 1; i -- ) // 从第一个非叶子节点开始shiftDown(i);}~MaxHeap() {delete[] data;}int size() {return count;}bool isEmpty() {return count == 0;} };
时间复杂度:O(n)
手撕排序
#include <iostream> #include <vector>using namespace std; void ShiftDown(vector<int>& arr, int k, int n) {// 从0开始索引的,所以判断时需要加1while (2 * k + 1 < n) {int j = 2 * k + 1;if (j + 1 < n && arr[j + 1] > arr[j])j += 1;if (arr[j] < arr[k])break;swap(arr[j], arr[k]);k = j;} } void heapify(vector<int>& arr, int n) {// 注意,此时我们的堆是从0开始索引的// 从(最后一个元素的索引-1)/2开始// 最后一个元素的索引 = n-1for (int i = (n - 2) / 2; i >= 0; i--)ShiftDown(arr, i, n);// 到此为止只能说是遵守了最大堆规则// 并没有排序// 接下来的for循环实现从小到大排序for (int i = n - 1; i >= 0; i--) {swap(arr[0], arr[i]);ShiftDown(arr, 0, i);} }int main() {vector<int> arr{ 3,5,7,3,6,1,4 };int n = arr.size();heapify(arr, n);for (int i = 0; i < n; i++)cout << arr[i] << " ";return 0; }
【数据结构】排序算法及优化整理相关推荐
- C++基础-介绍·数据结构·排序·算法
C++基础-介绍·数据结构·排序·算法 特点 使用方向 RPC Data Struct 数据结构 栈 Stack 内存分配中的栈 队列 List 数组 Array 链表 LinkTable 树 Tre ...
- 数据结构-排序算法(c语言实现篇)
数据结构-排序算法(c语言实现篇) 排序算法是非常常用的算法,从介绍排序的基本概念,到介绍各种排序算法的思想.实现方式以及效率分析.最后比较各种算法的优劣性和稳定性. 1 排序的概念及应用 1.1 排 ...
- 数据结构---排序算法的总结
数据结构-排序算法的总结 分类 冒泡排序,时间复杂度O(n x n),空间复杂度O(1),稳定 简单选择排序,时间复杂度O(n x n),空间复杂度O(1),不稳定 希尔排序,时间复杂度O(n^1.3 ...
- 【数据结构排序算法系列】数据结构八大排序算法
排序算法在计算机应用中随处可见,如Windows操作系统的文件管理中会自动对用户创建的文件按照一定的规则排序(这个规则用户可以自定义,默认按照文件名排序)因此熟练掌握各种排序算法是非常重要的,本博客将 ...
- 数据结构-排序算法总结与感悟
数据结构-排序算法总结 一,排序的基本概念 排序:有n个记录的序列{R1,R2,-,Rn},其相应关键字的序列是{K1,K2, -,Kn },相应的下标序列为1,2,-, n.通过排序,要求找出当前下 ...
- 算法基础-十大排序算法及其优化(文末有抽奖福利哦)
算法基础-十大排序算法及其优化 算法基础-十大排序算法及其优化 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kw1LA5Q4-1607527572080)(/uplo ...
- 图解选择排序算法及优化
作者:敲代码の流川枫 博客主页:流川枫的博客 专栏:和我一起学java 语录:Stay hungry stay foolish 工欲善其事必先利其器,给大家介绍一款超牛的斩获大厂offer利器--牛客 ...
- Python数据结构常见的八大排序算法(详细整理)
前言 八大排序,三大查找是<数据结构>当中非常基础的知识点,在这里为了复习顺带总结了一下常见的八种排序算法. 常见的八大排序算法,他们之间关系如下: 排序算法.png 他们的性能比较: 下 ...
- 数据结构——排序算法(含动态图片)
目录 插入排序 交换排序 选择排序 归并排序 常用排序算法复杂度和稳定性总结 前言 排序是<数据结构>中最基本的学习内容.排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行 ...
最新文章
- ubuntu14.04连接网络 No valid active connections found!
- 史迪仔的原型_星际宝贝三个版本对比,莉罗抛弃史迪仔,童年真的回不去了
- aix oracle 10.2.0.1 升级 10.2.0.4,AIX Oracle RAC 升级到10.2.0.4.0要特别注意的问题 - 爱肯的专栏 ......
- OMA 设备管理的通知发起的会话OMA Device Management Notification Initiated Session
- 订单状态 css_CSS状态2019
- 【华为敏捷/DevOps实践】3. 如何开好站立会议
- 拓步T66Ⅱ(牛牛2)Root教程
- c++ 实现一个object类_一个Java类就能实现微服务架构的权限认证
- go语言导出oracle数据,Go语言导出内容到Excel的方法
- HTML5中的绘图SVG VS Canvas
- AI+Science系列(一) :飞桨加速CFD(计算流体力学)原理与实践
- 欠采样和过采样_过采样和欠采样
- 80286 与 80386,实模式与保护模式切换编程
- 面向对象设计的新视角
- 《DSP using MATLAB》Problem 7.36
- 高德地图定位、添加定位图标、连线(二)
- 提升项目经理的有效路径之一:学习PMP项目管理
- 【剑指offer-54】20190907/03 字符流中第一个不重复的字符
- 批量拿webshell工具【最新】
- RuntimeError:a leaf Variable that requires grad has been used in an in-place
热门文章
- 成电计算机学院保研率,985一条街的街友们,我就想问问电子科技大学(成电)到底是什么水平的学校啊!...
- python和log有啥区别_细说 Python logging
- android aar项目_介绍如何调试Xamarin.Android的binding项目
- vim 格式化json
- 【tomcat】调整内存大小
- mybatis 原理_图解源码 | MyBatis的Mapper原理
- 一个html5页面,html5做一个黑板报页面
- win7访问linux共享路径不存在,win7系统访问网络共享找不到网络路径如何解决
- oracle删除判断是否存在,oracle创建表之前判断表是否存在,如果存在则删除已有表...
- php 语句,php的控制语句