数据结构与算法笔记——排序篇

本文详细介绍了排序算法,包括插入排序(直接插入、折半插入、希尔排序)、交换排序(冒泡排序、快速排序及其优化)、选择排序(直接选择、堆排序)、归并排序及其应用、计数排序、基数排序和桶排序。针对每种排序算法,阐述了其基本思想、时间复杂度和应用场景。在工程实践中,根据排序样本量、稳定性需求和时间效率选择合适的排序算法至关重要。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

 

 

前言

记录数据结构中的排序方法和经典应用。


一、插入排序

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提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值