【C++】【排序算法】快速排序;堆排序;归并排序;逆序对(保证看懂!)

目录

一、快速排序 

1.2快速排序拓展:基于快排的C++sort()函数(为什么你总是看不懂?) 

二、堆排序 

8.1.2.   堆常用操作¶

堆的存储与表示¶

元素入堆¶

堆顶元素出堆¶

8.1.4.   堆常见应用¶

三、归并排序 

 归并排序的拓展(1):数组中的逆序对

归并排序的拓展(2):重要逆序对


912. 排序数组 - 力扣(Leetcode)

一、快速排序 

基础版:取最左元素为哨兵:

/* 元素交换 */
void swap(vector<int>& nums, int i, int j) {
    int tmp = nums[i];
    nums[i] = nums[j];
    nums[j] = tmp;
}

/* 哨兵划分 */
int partition(vector<int>& nums, int left, int right) {
    int pivot=nums[left];//nums[left]作为基准数
    int i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= pivot)
            j--;          // 从右向左找首个小于基准数的元素
        while (i < j && nums[i] <= pivot)
            i++;          // 从左向右找首个大于基准数的元素
        swap(nums, i, j); // 交换这两个元素
    }
    swap(nums, i, left);  // 将基准数交换至两子数组的分界线
    return i;             // 返回基准数的索引
}

优化:随机选取一个哨兵: 

//912. 排序数组-快速排序
class Solution {
public:
    /*4、i指针指向<=哨兵的元素队列的最右边的元素的位置。*/
    int partition(vector<int>& nums, int l, int r){
        int pivot=nums[r];            //哨兵的值。
        int i=l-1;                    //初始没排序,没有元素比哨兵小,i指向-1。
        for(int j=l;j<=r-1;++j){      //j从l开始遍历,不是从0开始! j<=r-1因为nums[r]是哨兵,不用排它。
            if(nums[j]<=pivot){       //如果遇到<=哨兵的元素。
                i=i+1;                //i+1,指向比第一个哨兵大的元素的位置,也是比哨兵小的最右边的元素的后一个位置。
                swap(nums[i],nums[j]);//交换比哨兵大的元素和当前遍历的元素,当前遍历的元素就去到了哨兵小的元素的队列的最右边(比哨兵小的元素的队列长度加一了哟)。
            }
        }
        swap(nums[i+1],nums[r]);       //再把哨兵换回来。i+1,指向比第一个哨兵大的元素的位置。
        return i+1;                    //返回哨兵的位置。
    }

    /*3、随机选一个哨兵,把哨兵与最后元素交换,返回排好后的哨兵的位置*/
    int randomized_partion(vector<int>& nums, int l, int r){
        int i=rand()%(r-l+1)+l;        // 随机选一个作为我们的主元
        swap(nums[i],nums[r]);
        return partition(nums,l,r);
    }

    /*快排-1、将这一列排序,返回排序的哨兵的位置 2、并把这一列递归地【划分】为左右两列*/
    void randomized_quicksort(vector<int>& nums, int l, int r){
        if(l<r){
            int pos=randomized_partion(nums,l,r);
            randomized_quicksort(nums,l,pos-1);
            randomized_quicksort(nums,pos+1,r);
        }
    }

    /*主程序*/
    vector<int> sortArray(vector<int>& nums) {
        int n=nums.size();
        randomized_quicksort(nums,0,n-1);
        return nums;
    }
};

1.2快速排序拓展:基于快排的C++sort()函数(为什么你总是看不懂?) 

二、堆排序 

 堆排序部分引用/转载的是K大的《hello 算法》,原文地址:8.1.   堆 - Hello 算法

堆 (Heap)是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:

  • 「大顶堆 Max Heap」,任意结点的值 ≥ 其子结点的值;
  • 「小顶堆 Min Heap」,任意结点的值 ≤ 其子结点的值;

  • 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
  • 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。
  • 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。

8.1.2.   堆常用操作

值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」,其是一种抽象数据结构,定义为具有出队优先级的队列

而恰好,堆的定义与优先队列的操作逻辑完全吻合,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。

堆的常用操作见下表,方法名需根据编程语言确定。

方法名 描述 时间复杂度
push() 元素入堆 �(log⁡�)
pop() 堆顶元素出堆 �(log⁡�)
peek() 访问堆顶元素(大 / 小顶堆分别为最大 / 小值) �(1)
size() 获取堆的元素数量 �(1)
isEmpty() 判断堆是否为空 �(1)

 8.1.3.   堆的实现

下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 ≥ 替换为 ≤ )即可,有兴趣的同学可自行实现。

堆的存储与表示

在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,因而我们采用「数组」来存储「堆」

二叉树指针。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,而结点指针通过索引映射公式来实现

具体地,给定索引 i ,那么其左子结点索引为 2i+1 、右子结点索引为 2i+2 、父结点索引为 (i−1)/2 (向下整除)。当索引越界时,代表空结点或结点不存在。

元素入堆

给定元素 val ,我们先将其添加到堆底。添加后,由于 val 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,因此需要修复从插入结点到根结点这条路径上的各个结点,该操作被称为「堆化 Heapify」。

考虑从入堆结点开始,从底至顶执行堆化。具体地,比较插入结点与其父结点的值,若插入结点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个结点;直至越过根结点时结束,或当遇到无需交换的结点时提前结束。

/* 元素入堆 */
void push(int val) {
    // 添加结点
    maxHeap.push_back(val);
    // 从底至顶堆化
    siftUp(size() - 1);
}

/* 从结点 i 开始,从底至顶堆化 */
void siftUp(int i) {
    while (true) {
        // 获取结点 i 的父结点
        int p =  parent(i);
        // 当“越过根结点”或“结点无需修复”时,结束堆化
        if (p < 0 || maxHeap[i] <= maxHeap[p])
            break;
        // 交换两结点
        swap(maxHeap[i], maxHeap[p]);
        // 循环向上堆化
        i = p;
    }
}

堆顶元素出堆

堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:

  1. 交换堆顶元素与堆底元素(即交换根结点与最右叶结点);
  2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
  3. 从根结点开始,从顶至底执行堆化

顾名思义,从顶至底堆化的操作方向与从底至顶堆化相反,我们比较根结点的值与其两个子结点的值,将最大的子结点与根结点执行交换,并循环以上操作,直到越过叶结点时结束,或当遇到无需交换的结点时提前结束。

/* 元素出堆 */
void pop() {
    // 判空处理
    if (empty()) {
        throw out_of_range("堆为空");
    }
    // 交换根结点与最右叶结点(即交换首元素与尾元素)
    swap(maxHeap[0], maxHeap[size() - 1]);
    // 删除结点
    maxHeap.pop_back();
    // 从顶至底堆化
    siftDown(0);
}

/* 从结点 i 开始,从顶至底堆化 */
void siftDown(int i) {
    while (true) {
        // 判断结点 i, l, r 中值最大的结点,记为 ma
        int l = left(i), r = right(i), ma = i;
        // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出
        if (l < size() && maxHeap[l] > maxHeap[ma]) 
            ma = l;
        if (r < size() && maxHeap[r] > maxHeap[ma])
            ma = r;
        // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出
        if (ma == i) 
            break;
        swap(maxHeap[i], maxHeap[ma]);
        // 循环向下堆化
        i = ma;
    }
}

8.1.4.   堆常见应用

  • 优先队列。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 O(log⁡ n) ,建队操作为 O(n) ,皆非常高效。
  • 堆排序。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。
  • 获取最大的 k 个元素。这既是一道经典算法题目,也是一种常见应用,例如选取热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。

三、归并排序 

class Solution {
    vector<int> tmp;
    void mergeSort(vector<int>& nums, int l, int r) {
        /*1.递归终止:左指针大于等于右指针*/
        if (l >= r) return;
        
        /*2.分组*/
        int mid = (r-l)/2+l;  //错误1:不是(l+r)/2-l....
        mergeSort(nums, l, mid);
        mergeSort(nums, mid + 1, r);

        /*3.每组分组排序,用tmp临时储存,再一个个赋值给原数组nums*/
        int i = l, j = mid + 1;        //两组的指针
        int cur = 0;                   //tmp指针,指出tmp数组的哪一位置来储存,储存两组中较小的元素
        while (i <= mid && j <= r) {   //当两组指针都没有到数组终点时
            if (nums[i] <= nums[j]) {
                tmp[cur] = nums[i];
                cur++;i++;
            }
            else {
                tmp[cur] = nums[j];
                cur++;j++;
            }
        }
        while (i <= mid) {
            tmp[cur] = nums[i];
            cur++;i++;
        }
        while (j <= r) {
            tmp[cur] = nums[j];
            cur++;j++;
        }
        /*再一个个赋值给原数组nums*/
        for (int i = 0; i < r - l + 1; ++i) { //排序的元素个数:r-l+1
            nums[l+i] = tmp[i];  //排序的位置从nums[l]到nums[r]     //错误2:是temp[i]不是temp[cur]
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize((int)nums.size(), 0);     //错误3:忘记写这个了 v.resize(amount,initial val)
        mergeSort(nums, 0, (int)nums.size() - 1);
        return nums;
    }
};

 归并排序的递归也可以写成不用return的形式:

void MergeSort(vector<int>& nums,vector<int>&temp,int l,int r){
        if(l<r){
            /*分组*/
            int mid=(r-l)/2+l;
            MergeSort(nums,temp,l,mid);
            MergeSort(nums,temp,mid+1,r);
            /*排序*/
            mergesort(nums,temp,l,mid,mid+1,r);
        }
    }

 归并排序的拓展(1):数组中的逆序对

剑指 Offer 51. 数组中的逆序对 - 力扣(Leetcode)

找出数组中的逆序对,可以用归并排序来遍历。其中最关键的一步是如何找跨组的逆序对。即逆序对一个数在[l,mid]中,一个数在[mid+1,r]中,并且保证统计是完全的、没有遗漏的。

方法:按照逆序对的要求,如果如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。i指向的组在j指向的组的前面,并且i的组和j的组已经各自按照升序排好序。也就是说,如果nums[i]>nums[j],nums[i]之后的数(直到nums[mid])又都是比nums[i]大的,说明nums[i]之后的数都大于nums[j],就找到了跨组的逆序对。并且这种找法是能够找全的。

举个例子:找出[7,5,6,4]的逆序对个数。

通过归并排序,第一轮排序: 

 在比较7和5时发现了一个逆序对[7,5],count++count=1,在比较[6,4]时发现了一个逆序对,count++,count=2。

第二轮排序:

在比较5和4时,5>4,即nums[i]>nums[j],并且[5,7]已经按升序排序,那么说明5后面到mid的数([7])都>4,这次比较找出的逆序对是[5,4]和[7,4],共有mid-i+1个,所以count+=mid-i+1个。所以这次count+=1-0+1=2,count=4,

在比较7和6时又出现nums[i]>nums[j],count+=1-1+1=1个,count=5

答案:5

统计逆序对个数的代码:

            while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[cnt++] = nums[i++];
            }
            else {
                tmp[cnt++] = nums[j++];
                count+=mid-i+1;//关键。见注释
            }

 其余的代码就是归并排序。

class Solution {
private:
    
public:
    void mergeSort(vector<int>& nums, vector<int> &tmp,int &count,int l, int r) {
        if (l >= r) return;
        int mid = (r-l) /2+l;
        mergeSort(nums,tmp,count, l, mid);
        mergeSort(nums,tmp,count, mid + 1, r);
        int i = l, j = mid + 1;
        int cnt = 0;
        while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[cnt++] = nums[i++];
            }
            else {
                //如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
                tmp[cnt++] = nums[j++];
                count+=mid-i+1;//关键。见注释
            }
        }
        while (i <= mid) {
            tmp[cnt++] = nums[i++];
        }
        while (j <= r) {
            tmp[cnt++] = nums[j++];
        }
        for (int i = 0; i < r - l + 1; ++i) {
            nums[i + l] = tmp[i];
        }
    }

    int reversePairs(vector<int>& nums) {
        vector<int> tmp;
        int count=0;
        tmp.resize((int)nums.size(), 0);
        mergeSort(nums,tmp,count, 0, (int)nums.size() - 1);
        return count;
    }
};

归并排序的拓展(2):重要逆序对

OpenJudge - M:重要逆序对

描述

给定N个数的序列a1,a2,...aN,定义一个数对(ai, aj)为“重要逆序对”的充要条件为 i < j 且 ai > 2aj。求给定序列中“重要逆序对”的个数。

输入

本题有多个测试点,每个测试点分为两行:第一行为序列中数字的个数N(1 ≤ N ≤ 200000),第二行为序列a1, a2 ... aN(0 ≤a ≤ 10000000),由空格分开。N=0表示输入结束。

输出

每个测试点一行,输出一个整数,为给序列中“重要逆序对”的个数。

样例输入

10
0 9 8 7 6 5 4 3 2 1
0

样例输出

16

注意不能把重要逆序对的判断写在判断nums[i]和nums[j]里面,会多一重循环,超时。

超时的答案:

#include <iostream>
 
using namespace std;
 
long long sum = 0;
 
 
void merge(int *s, int *temp, int startIndex, int endIndex, int mid)
{
    int i = startIndex, j = mid + 1, k = startIndex;
    int pointer = startIndex;
 
    while(i <= mid && j <= endIndex)
    {
        if(s[i] > s[j])
        {
            temp[k] = s[j];
 
            while(s[pointer] <= 2 * s[j] && pointer <= mid)
            {
                pointer ++;
            }
            if(pointer != mid + 1)
            {
                sum += mid - pointer + 1;
            }
 
            j ++;
        }
        else
        {
            temp[k] = s[i];
            i ++;
        }
        k ++;
    }
 
    while(i <= mid)
    {
        temp[k ++] = s[i ++];
    }
    while(j <= endIndex)
    {
        temp[k ++] = s[j ++];
    }
    for(int i = startIndex; i <= endIndex; i ++)
    {
        s[i] = temp[i];
    }
 
}
 
void mergeSort(int *s, int *temp, int startIndex, int endIndex)
{
    int mid = (startIndex + endIndex) / 2;
 
    if(startIndex < endIndex)
    {
        mergeSort(s, temp, startIndex, mid);
        mergeSort(s, temp, mid + 1, endIndex);
        merge(s, temp, startIndex, endIndex, mid);
    }
}
 
 
int s[200005] = {};
int temp[400010] = {};
 
int main(){
 
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        cin >> s[i];
    }
    mergeSort(s, temp, 0, n - 1);
    cout << sum << endl;
    return 0;
}

正确的写法是把重要逆序对的判断写在外面:

#include <iostream>
using namespace std;
 
int arr[200005];
int tmp[200005];
int N;
 
long long mergesort(int start,int end){
    long long cnt=0, mid=(start+end)/2;
    if(start>=end) return 0;
    cnt+=mergesort(start,mid);
    cnt+=mergesort(mid+1,end);
    //find important reverse pair
    int i=start,j=mid+1;
    while(i<=mid && j<=end){
        if(arr[i]>2*arr[j] && j<=end){
            cnt+=(mid-i+1);
            j++;
        }else{
            i++;
        }
    }
    //merge sort
    i=start,j=mid+1;
    int idx=0;
    while(i<=mid && j<=end){
        if(arr[i]>arr[j]){
            tmp[idx++]=arr[j++];
        }else{
            tmp[idx++]=arr[i++];
        }
    }
    while(i<=mid) tmp[idx++]=arr[i++];
    while(j<=end) tmp[idx++]=arr[j++];
    for(int k=0;k<idx;k++){
        arr[start+k]=tmp[k];
    }
    return cnt;
}
 
int main(){
    while(cin>>N){
        if(N==0) break;
        for(int i=1;i<=N;i++){
            cin>>arr[i];
        }
        cout<<mergesort(1,N)<<endl;
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/icecreamTong/article/details/128424838