提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
记录数据结构中的排序方法和经典应用。
一、插入排序
1.直接插入排序
插入排序思想: 将逐步调整数组使其划分为有序与无序两部分。(有些题目可能可以利用这特点解题)
插入排序最好时间复杂度: O(n), 最坏与平均时间复杂度为O(n2),在样本量数据小的情况下,工程中排序算法会选择插入排序。
注意:S[0]哨兵的设置,即用于临时存储正在排序的一个元素,又使判断排序位置最多终止在S[1],即不需要判断j>0,因为J比较时一定大于。l
void insertSort(vector<int> &s){
int length = s.size() - 1;
//把第一个位置空掉,用于暂时存储正在排序的元素
int i,j;
for(i = 2; i <= length; i++){
s[0] = s[i];
for(j = i - 1; s[j] > s[0]; j--) //由于s[0] = s[i]比较最多到s[1]就停止啦
s[j+1] = s[j];
s[j+1] = s[0];
}
}
2.折半插入排序
先进行折半查找,再后移插入位置后面的元素并插入元素,折半查找完成后,low指向比插入数值大一点的元素,high指向小一点的元素
void BInsertSort(vector<int> &s){
int length = s.size() - 1;
//把第一个位置空掉,用于暂时存储正在排序的元素
int i,j;
int low,mid,high;
for(i = 2; i <= length; i++){
s[0] = s[i];
low = 1; high = i - 1;
while(low <= high){
mid = (low + high) / 2;
if(s[mid] > s[i])
high = mid - 1;
else
low = mid + 1;
}
for(j = i - 1; j >= high + 1; j--) //由于s[0] = s[i]比较最多到s[1]就停止啦
s[j+1] = s[j];
s[high+1] = s[0];
}
}
3.希尔排序(增量排序)
void shellSort(vector<int> &s){
int i,j;
int length = s.size() - 1;
for(int dk = length / 2; dk >= 1; dk = dk/2) //每次步长,分组
for(int i = dk + 1; i <= length; i++){ //多组同时进行直接插入排序
s[0] = s[i];
for (j = i - dk; j > 0&&s[j] > s[0]; j -= dk)
//j可能为负的,例如步长为3,第二组的j最多可以减到2 - 3 = -1
//哨兵失去了检查每次比较排序结束的判断条件
s[j + dk] = s[j];
s[j + dk] = s[0];
}
}
二、交换排序
1.冒泡排序
时间复杂度:O(n2),一般不做使用
每次冒泡都会把当前最大值,传递到最后一个位置,可以加一个flag标记,若相邻两个元素,没有前面大于后面的,则说明已排序完成,可以直接结束排序
void bubbleSort(vector<int> &vec){
int n = vec.size() - 1;
for(int i = 1; i < n; i++){
bool flag = false; //N次冒泡
for(int j = 1; j <= n - i; j++)
if(vec[j] > vec[j + 1]){
vec[0] = vec[j + 1];
vec[j + 1] = vec[j];
vec[j] = vec[0];
flag = true;
}
if(flag == false) return ;
}
}
2.快速排序
快速排序,一次划分Parition确定一个基准的位置,分治思想
平均时间复杂度:O(nlogn),根据递归函数master复杂度计算公式计算。额外空间消耗为存储每次划分的边界点
与归并排序相比的优势:常数项比归并排序低,不需要额外开辟O(n)空间,所以工程排序算法在排序内置数据类型的数据即不要求稳定性时,选用快排
缺点:无法保持稳定性
经典快速排序的写法:
//快速排序,一次划分Parition确定一个基准的位置,O(n)
void Swap(int &a, int &b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
//经典划分的写法有:
//1. 快排划分的双指针单向扫描分区法
//分为:左边小于等于,右边大于
int P_Parition(int a[], int low, int high){
int pivot = a[low]; // 设置主元
int more = high;
int cur = low + 1;
while(cur <= more){ // 注意必须交错
if(a[cur] <= pivot)
cur++;
else
Swap(a[low], a[more--]);
}
Swap(a[low], a[cur]);
return more;
}
//双向扫描法
//分为:左边小于等于,右边大于
int PP_Parition(int a[], int low, int high){
int pivot = a[low];
int cur = low + 1;
int more = high;
while(cur <= more){
while(cur <= more && a[cur] <= pivot)
cur++;
while(cur <= more && a[more] > pivot)
more--;
if(cur < right)
Swap(a[cur], a[more]);
}
// more指向小于等于边界,cur来到大于边界
Swap(a[low], a[more]);
return right;
}
void quickSort(vector<int> &vec, int low, int high){ //O(n*logn)
//确定递归终止条件,从上往下面递归,先计算后递归
if(low < high){ //当low = high 时,元素基准不需要划分确定位置
int pos = Parition1(vec, low, high);
printf("pos = %d\n", pos);
quickSort(vec, low, pos - 1);
quickSort(vec, pos + 1, high);
}
}
荷兰国旗问题改进快排:三指针分区法
荷兰国旗问题: 将一个数组按照某个目标数划分为小于,等于,大于的三部分数组,并返回边界点
//改进之后划分的写法
//荷兰国旗写法
vector<int> HE_Parition(vector<int> &vec, int low, int high, int num){
int less = low - 1; //小于num的区域
int more = high + 1; //大于num的区域
int cur = low; //当前所在位置,此区域为相等
while(cur < more){
if(vec[cur] < num)
swap(vec[cur++], vec[++less]); //被小于区域推着走
else if(vec[cur] > num)
swap(vec[cur], vec[--more]); //注意不被大于区域推着走
else{ // == num
cur++;
}
}
return vector<int>({less + 1, more - 1});
}
//使用荷兰国旗问题改进快排
vector<int> Parition(vector<int> &vec, int low, int high){
int less = low - 1;
int more = high; // 以最后的一个数为基准, low - 1],....,[more,比上面少开一个空间
while(low < more){
if(vec[low] < vec[high])
swap(vec[low++], vec[++less]);
else if(vec[low] > vec[high])
swap(vec[low], vec[--more]);
else
low++;
}
swap(vec[more], vec[high]);
return vector<int>({less + 1, more}); // 划分点
}
void quickSort(vector<int> &vec, int low, int high){
if(low < high){
// int rand = low + (int )Math::random() * (high - low + 1);
// swap(vec[rand], vec[high]); //随机快排
vector<int> p = Parition(vec, low, high);
quickSort(vec, low, p[0] - 1);
quickSort(vec, p[1] + 1, high);
}
}
快速排序的优化(优化选取基准):
三点中值法:在l, mid, r三点选择中间值作为基准
随机快排:概率随机选取基准
快速排序的经典问题:
1.荷兰国旗问题
2.TopK问题
三、选择排序
1.直接选择排序
时间复杂度:O(n2),一般使用较少
//选择排序
void selectSort(vector<int> &vec, int n){
int temp;
for(int i = 1; i < n; i++){
int min = i; //记录最小值是否发生变化
for(int j = i + 1; j <= n; j++)
if(vec[j] < vec[min]){ //找出最小的值的下标
min = j; //不需每次进行交换
}
if(min != i){
temp = vec[i];
vec[i] = vec[min];
vec[min] = temp;
}
}
}
2.堆排序
时间复杂度:O(NlogN)
//堆排序
//1.堆的初始化
//向下调整, 选择K值,将K为根结点的树,临时变量vec[0]存储原栈顶
//确定第一层孩子结点的最大值vec[i],若vec[i]小于原堆顶元素vec,将双亲结点来存储原栈顶vec[0]
//若vec[0] >= vec[i],
void adjustDown(vector<int> &vec, int k, int len){ //向下调整,适用于堆的初始化构建过程
vec[0] = vec[k];
for(int i = 2 * k; i <= len; i *= 2 ){
if(i < len && vec[i] < vec[i + 1])
i++;
if(vec[0] >= vec[i]) //每次比较的都是原堆顶的关键字值
break;
else{ //若孩子结点存在堆顶的元素,
vec[k] = vec[i]; //则将孩子结点的复制一份给双亲结点
k = i;
}
}
vec[k] = vec[0]; //直到出现,孩子结点的值都不大于栈顶元素
//就将该值赋值到
}
void adjustUp(vector<int> &vec, int k){ //向上调整,仅适用于堆的插入过程
vec[0] = vec[k];
int i = k / 2;
while(i > 0 && vec[i] < vec[0]){
vec[k] = vec[i];
k = i;
i = k / 2;
}
vec[k] = vec[0];
}
void buildMaxHeap(vector<int> &vec, int len){
for( int i = len / 2; i >= 1; i--) //从后面开始,对每一个分支结点排序
adjustDown(vec, i, len);
}
void heapSort(vector<int> &vec, int len){
buildMaxHeap(vec, len);
for(int i = len; i > 1; i--){
int temp = vec[i];
vec[i] = vec[1];
vec[1] = temp;
adjustDown(vec, 1, i - 1);
}
}
堆排序的经典应用:
1.Topk问题
2.中位流问题
四、归并排序
时间复杂度:O(NlogN)
额外空间复杂度:O(N)
void Merge(vector<int> &vec,int low, int mid, int high){
vector<int> temp(vec);
int i = low, j = mid + 1, k = low;
while(i <= mid && j <= high){
if( temp[i] > temp[j])
vec[k++] = temp[j++];
else
vec[k++] = temp[i++];
}
while( i <= mid)
vec[k++] = temp[i++];
while( j <= high)
vec[k++] = temp[j++];
}
void mergeSort(vector<int> &vec, int low, int high){
if(low < high){
int mid = (low + high) / 2;
mergeSort(vec, low, mid);
mergeSort(vec, mid + 1, high);
Merge(vec, low, mid, high);
}
}
归并排序的经典应用:
1.小和问题
2.逆序对问题
工程中排序算法的选取:
1. 排序样本量小,选择插入排序
2.不要求稳定性,选择快速排序
3.要求稳定性,选择归并排序
前面都是基于比较的排序算法,后面为非比较的排序算法
基于比较排序算法的应用问题:
1. 调整数组顺序——使奇数左边偶数右边,要求时间复杂度O(N)
快排: 无法保证稳定性
归排:可以保证稳定性
2.TopK问题——乱序大量数据找第K大(或小)的数
快速排序划分+若划分不对,再进行二分选区: 期望O(N),最差O(N2),且需要改变数组的内容
堆排序
3.取出数组出现次数超过一半的数字
解放1:排序+取中间的数字
解放2:Hash统计
解放3:顺序统计,找第N/2大的划分
解放4:不同的数消除法:设置候选变量和出现次数,出现次数为0设置新的候选,候选与当前元素不同,出现次数减一,相同加一
4.最小可用ID
五、计数排序
计数排序:
用一个额外的计数数组C,根据数组C来将原数组A中的元素排到正确的位置
分类:内部非比较排序
数据结构: 数组
计数排序步骤:
(1)统计数组A中每个值A[i]出现的次数,并存入C[A[i]]
(2)从前到后,使数组C中每个值等于其与前面一项相加,数组C[A[i]]就变成了代表数组A中小于等于A[i]的元素个数
(3)反向填充目标数组B:将数组元素A[i]放在数组B的第C[A[i]]个的位置,每放一个元素就将C[A[i]]递减。
最差、最好和最平均时间复杂度均为:O(n + k), k与排序最大的元素有关
空间复杂度:O(n + k)
稳定性:稳定
应用限制:
(1)由于借助数组C下标的顺序计数,不太适用于非整数排序,若是非整数元素则需要按大小转换成映射整数,再进行顺序计数。
(2)由于计数数组C大小与排序整数元素大小范围有关,对于含太大整数元素的排序不适合使用,以免浪费大量时间和内存。
测试实例:
#include<bits/stdc++.h>
using namespace std;
//计数排序应用:
const int K = 100; //排序[0~99]内的整数
int Count[K] = {0}; //根据计数数组下标顺序计算实际排序的位置
void CountSort(int A[], int N, int B[]){
for(int i = 0; i < N; i++) //注意1:注意排序是否包括A[0]
Count[A[i]]++;
for(int i = 1; i < K; i++)
Count[i] += Count[i - 1];
for(int i = N - 1; i >= 0; i--) //注意2:从后往前扫,保证稳定性
B[--Count[A[i]]] = A[i]; //注意3:若排序元素包括A[0],
//这里B[0]也要考虑是第一个位置
//因为是从0开始的,而Count[A[i]]统计的是第几个数,
//所以必须要使用--Count[A[i]],先减一,这里我掉入坑了
}
int main(){
int N = 10; //排序元素个数
int A[N] = {0, 3, 5, 12, 7, 20, 5, 0, 7, 14};
int B[N] = {0}; //排序后存放数组
CountSort(A, N, B);
for(int i = 0; i < N; i++)
cout << B[i] << endl;
}
六、基数排序
基数排序:
将所有待比较的正整数统一为同样的数位长度,数位较短就往前面补零。然后从最低位开始进行基数为10的计数排序,一直到最高位计数排序完后,数列就变成一个有序序列。(利用了计数排序的稳定性,对于高位相同的零的顺序不变)
分类:内部非比较排序
数据结构:数组
最差、最好和平均时间复杂度:O(n * dn)
空间复杂度:O(n * dn)
稳定性:稳定
#include<bits/stdc++.h>
using namespace std;
const int dn = 3; //待排序元素的最大位数
const int k = 10; //计数排序的基数为10
int Count[k] = {0};
//获取x的第d位数字
int GetDigit(int x, int d){
int radix[] = {1, 1, 10, 100};
return (x / radix[d]) % 10; //短位数字前位获取到的数字为0,计数按照0计数
}
void CountSort(int A[], int n, int d){
for(int i = 0; i < k; i++)
Count[i] = 0;
for(int i = 0; i < n; i++)
Count[GetDigit(A[i], d)]++;
for(int i = 1; i < k; i++)
Count[i] += Count[i - 1];
int *p = (int *)malloc(n*sizeof(int));
if(!p) return ;
for(int i = n - 1; i >= 0; i--)
p[--Count[GetDigit(A[i], d)]] = A[i];
for(int i = 0; i < n; i++)
A[i] = p[i];
free(p);
}
void LsdRadixSort(int A[], int n){
for(int d = 1; d <= dn; d++)
CountSort(A, n, d);
}
int main(){
const int N = 6;
int A[N] = {100, 11, 5, 5, 6, 2};
LsdRadixSort(A, N);
for(int i = 0; i < N; i++)
cout << A[i] << endl;
}
七、桶排序
桶排序:
将数组元素映射到有限数量个桶里,利用计数排序可以定义桶的边界,每个桶再各自进行桶内的排序(使用其他的排序算法或递归继续桶排序)。
分类:内部非比较排序
最差时间复杂度:O(nlogn)或O(n^2),取决于桶内的排序方式
最优时间复杂度:O(n),每个元素占一个桶
平均时间复杂度:O(n),保持每个桶内元素个数均匀
空间复杂度:O(n + bn)
稳定性:稳定
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。