排序算法
1.排序的分类概述
排序的定义
对一序列对象根据某个关键字进行排序
术语说明
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b前面。
不稳定:如果a原本在b前面,而a=b,排序之后a可能出现在b的后面
内排序:所有排序操作都在内存中完成
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行
时间复杂度:一个算法执行所消耗的时间
空间复杂度:运行完一个程序所需内存大小
算法总结
参考文章:
2.冒泡排序、选择排序、插入排序
此部分较为基础,附上参考链接:
3.归并排序
归并排序的流程
其c++代码如下所示:
//使用递归对数组data从索引i到索引j之间的元素排序
void MergeSort(vector<int>& data, int i, int j) {
if (j == i) return;//递归结束条件
int mid = i + (j - i) / 2;
MergeSort(data, i, mid);//对前半部份进行排序
MergeSort(data, mid + 1, j);//对后半部分进行排序
Sort(data, i, mid, j);//将两个有序数组归并成一个有序数组
}
此处Sort(data, i, mid, j)是合并两个有序数组,其合并逻辑如下:
Sort函数代码实现如下:
void Sort(vector<int>& data, int i, int mid, int j) {
//一个临时空间,存放排序好的数组,最后将这个数组赋值给data[i]到data[j]
vector<int> tmp(j - i + 1);
int idx1 = i;//数组1索引
int idx2 = mid + 1;//数组2索引
int k = 0;
//两个索引都没超出边界
while ((idx1<=mid)&&(idx2<=j)) {
if (data[idx1] <= data[idx2])
tmp[k++] = data[idx1++];
else tmp[k++] = data[idx2++];
}
//索引idx1超出数组1边界(数组1访问完毕)而索引idx2还未超出边界
while ((idx1 > mid) && (idx2 <= j))
tmp[k++] = data[idx2++];
//索引idx2超出数组2边界(数组2访问完毕)而索引idx1还未超出边界
while ((idx1 <= mid) && (idx2 > j))
tmp[k++] = data[idx1++];
//这一步对于data的索引很容易出错,需要弄清楚的是data是从左边界i开始的
for (int m = 0; m < j - i + 1; m++)
data[i + m] = tmp[m];
}
写成类封装(对比上面稍微改了点逻辑,保留了原始的data):
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
vector<int> Mergesort(vector<int>data, int right) {//right=data.size()-1
if (right == 0)return data;
int mid = right/ 2;
vector<int> data1(data.begin(), data.begin() + mid + 1);
vector<int> data2(data.begin() + mid + 1, data.end());
data1 = this->Mergesort(data1,mid);
data2 = this->Mergesort(data2, right - mid - 1);
return this->Sort(data1, data2, mid + 1, right - mid);
}
private:
vector<int>Sort(vector<int>data1, vector<int>data2, int size1, int size2) {
vector<int> tmp(size1 + size2);
int idx1 = 0, idx2 = 0, k = 0;
while (idx1 < size1 && idx2 < size2) {
if (data1[idx1] <= data2[idx2])tmp[k++] = data1[idx1++];
else tmp[k++] = data2[idx2++];
}
while (idx1 >= size1 && idx2 < size2)
tmp[k++] = data2[idx2++];
while (idx2 >= size2 && idx1 < size1)
tmp[k++] = data1[idx1++];
return tmp;
}
};
int main() {
Solution s;
vector<int> vec{ 9,8,7,6,5,4,3,2,1 };
vector<int>s1 = s.Mergesort(vec, vec.size() - 1);
for (auto it : s1)
cout << it << endl;
return 0;
}
参考文章:
- https://baijiahao.baidu.com/s?id=1661055490900110198&wfr=spider&for=pc
- https://www.jianshu.com/p/33cffa1ce613
4.堆排序
4.1准备知识
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序。
所谓完全二叉树即叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
存储一个完全二叉树,最适合使用数组,因为它相比链表不需要存储左、右子树的指针,更加节省内存空间,通过数组索引即可以随机访问到对应元素。
假设你知道某个节点下标为x ,那么其左子树下标为 x *2+1,其右子树下标为 x *2+2 ,其父节点下标为 (x-1)/2。
4.2大根堆和小根堆
每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;
每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。
4.3堆排序基本步骤
基本思想:
- 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
- 将顶端的数与末尾的数交换,此时末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
4.3.1MAX-HEAPIFY
MAX-HEAPIFY是用于维护最大堆性质的重要过程。它的输入为一个数组A和一个下标i。在调用MAX-HEAPIFY的时候,我们假定根节点为LEFT(i)和RIGHT(i)的二叉堆都是大根堆,但这时A[i]有可能小于其孩子,这样就违背了大根堆的性质。
MAX-HEAPIFY通过让A[i]的值在大根堆中逐级下降,从而是的以下标为i为根节点的字数重新遵循大根堆的性质。
void adjust(vector<int>& arr, int len, int idx) {
int left = 2 * idx + 1;//idx的左子节点
int right = 2 * idx + 2;//idx的右子节点
int maxidx = idx;
//判断最大节点是不是左右子节点
if (left<len && arr[left]>arr[maxidx])maxidx = left;
if (right<len && arr[right]>arr[maxidx])maxidx = right;
if (maxidx != idx) {
swap(arr[maxidx], arr[idx]);//交换根节点和子节点的值
adjust(arr, len, maxidx);//交换后的子节点值为插入的节点的值,继续迭代
}
}
在建堆的时候,我们可以用自底向上的方法利用过程MAX-HEAPIFY把一个大小为n的数组转换为大根堆。子数组A(n/2+1…n)中的元素都是树的叶节点。每个叶节点都可以看成只包含一个元素的堆。建堆的过程就是堆树中的其他节点都调用一次MAX-HEAPIFY:
for (int i = n / 2 - 1; i >= 0; i--)
//MAX-HEAPIFY(A,i);
adjust(arr, size, i);
4.3.2构造堆
以下建堆过程与上述描述无关,是直接建堆。
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
假设存在以下数组:
构造堆的流程如下:每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端。
注:下图中红色节点为添加节点,紫色节点为交换节点对
或者
4.3.3利用大根堆进行排序
根据之前的堆生成操作,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆。然后依次操作即可,如下图所示:
注:红色代表已经排序完的节点,紫色代表要交换的节点
4.4总结
将上述流程总结如下:
- 将无序数组构造出一个大根堆
- 固定一个最大值,将剩余的数重新构造一个大根堆,重复此过程直到所有数都固定完毕
堆排序c++代码如下:
/*堆排序*/
#include<iostream>
#include<vector>
using namespace std;
void heapSort(vector<int>& arr, int size);
void adjust(vector<int>& arr, int len, int idx);
//递归方式构建大根堆(len是arr的长度,idx是当前节点的下标)
void adjust(vector<int>& arr, int len, int idx) {
int left = 2 * idx + 1;//idx的左子节点
int right = 2 * idx + 2;//idx的右子节点
int maxidx = idx;
if (left<len && arr[left]>arr[maxidx])maxidx = left;
if (right<len && arr[right]>arr[maxidx])maxidx = right;
if (maxidx != idx) {
swap(arr[maxidx], arr[idx]);
adjust(arr, len, maxidx);
}
}
void heapSort(vector<int>& arr, int size) {
//构建大根堆,从最后一个节点的父节点开始向前遍历
for (int i = size / 2 - 1; i >= 0; i--)
adjust(arr, size, i);
//调整大根堆
for (int i = size - 1; i >= 1; i--) {
swap(arr[0], arr[i]);
adjust(arr, i, 0);
}
}
int main() {
vector<int> arr = { 8, 1, 14, 3, 21, 5, 7, 10 };
heapSort(arr, arr.size());
return 0;
}
参考文章:
5.希尔排序
有空再写,先附上个参考链接:
https://blog.csdn.net/lgl782519197/article/details/106000802
6.快速排序
参考视频:https://www.bilibili.com/video/BV1K44y1k79z
具体原理看上面链接里面的视频就够了,下面简单讲一下快排和附上c++代码:
对于包含n个数的输入数组来说,快速排序是一种最坏情况时间复杂度为O(n²)的排序算法。
虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能很好,它的期望时间复杂度为O(nlgn),而且O(nlgn)中隐含的常数因子非常小。
附代码:
//快速排序
void fastsort(vector<int>& nums, int left, int right) {
if (left >= right)return;
int q = partition(nums, left, right);
fastsort(nums, left, q - 1);
fastsort(nums, q + 1, right);
}
int partition(vector<int>& nums, int left, int right) {
int i = left;
for (int j = left; j <= right - 1; j++)
if (nums[j] <= nums[right])
swap(nums[i++], nums[j]);
swap(nums[i], nums[right]);
return i;
}