[C++] [Sorting Algorithm] Quick Sort; Heap Sort; Merge Sort; Reverse Order (Guaranteed to understand!)

Table of contents

1. Quick Sort 

1.2 Expansion of quick sorting: C++sort() function based on quick sorting (why do you always fail to understand?) 

2. Heap sort 

8.1.2. Common heap operations¶

Heap Storage and Representation¶

Elements into the heap¶

Heap top element out of the heap¶

8.1.4. Common applications of the heap¶

3. Merge sort 

 Extension of Merge Sort (1): Reversed pairs in an array

Extension of merge sort (2): important reverse order pairs


912. Sorting Array - Leetcode

1. Quick Sort 

Basic version: take the leftmost element as a sentinel:

/* 元素交换 */
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;             // 返回基准数的索引
}

Optimization: Randomly select a sentinel: 

//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 Expansion of quick sorting: C++sort() function based on quick sorting (why do you always fail to understand?) 

2. Heap sort 

 The heap sort part quotes/reprints the "hello algorithm" of K University, the original address: 8.1. Heap-Hello algorithm

Heap (Heap) is a "complete binary tree" under limited conditions . According to the establishment conditions, the heap is mainly divided into two types:

  • "Max Heap", the value of any node ≥ the value of its child nodes;
  • "Min Heap", the value of any node ≤ the value of its child nodes;

  • Since the heap is a complete binary tree, the nodes at the bottom layer are filled to the left, and the nodes at other layers are all filled.
  • The root node in the binary tree corresponds to the "top of the heap", and the rightmost node at the bottom corresponds to the "bottom of the heap".
  • For the big top heap/small top heap, the value of the top element (ie the root node) of the heap is the largest/smallest.

8.1.2. Common heap operations¶

It is worth noting that most programming languages ​​provide "Priority Queue", which is an abstract data structure defined as a queue with dequeue priority .

It just so happens that the definition of the heap is completely consistent with the operation logic of the priority queue . The big top heap is a priority queue in which elements are dequeued from large to small. From the perspective of usage, we can understand "priority queue" and "heap" as equivalent data structures. Therefore, this article and the code do not make a special distinction between the two, and use "heap" to name them uniformly.

The common operations of the heap are shown in the table below, and the method name needs to be determined according to the programming language.

method name describe time complexity
push() elements into the heap �(log⁡�)
pop() Heap top element out of heap �(log⁡�)
peek() Access the top element of the heap (large/small top heaps are the largest/smallest values ​​respectively) �(1)
size() Get the number of elements in the heap �(1)
isEmpty() Check if the heap is empty �(1)

 8.1.3. Heap implementation¶

The implementation below is "big top heap". If you want to convert it to "small top heap", just invert all size logic judgments (for example, replace ≥ with ≤ ). Interested students can implement it by themselves.

Heap Storage and Representation¶

In the chapter on binary trees, we learned that "complete binary tree" is very suitable to be represented by "array", and the heap happens to be a complete binary tree, so we use "array" to store "heap" .

Binary tree pointer . When using an array to represent a binary tree, the element represents the node value, the index represents the position of the node in the binary tree, and the node pointer is realized through the index mapping formula .

Specifically, given an index i , then its left child has an index of 2i+1 , its right child has an index of 2i+2 , and its parent has an index of (i−1)/2 (divisible downward). When the index is out of bounds, it means that the node is empty or the node does not exist.

 

Elements into the heap¶

Given an element  val , we first add it to the bottom of the heap. After adding, since  val it may be larger than other elements in the heap, the establishment condition of the heap may have been destroyed at this time, so it is necessary to repair each node on the path from the insertion node to the root node . This operation is called "heaping Heapify".

Consider starting from the entry node and performing heapization from bottom to top . Specifically, compare the value of the inserted node and its parent node, and exchange them if the inserted node is larger; and loop through the above operations, repairing each node in the heap from bottom to top; until the root node is crossed , or terminate early when a node that does not need to be exchanged is encountered.

/* 元素入堆 */
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;
    }
}

 

Heap top element out of the heap¶

The top element of the heap is the root node of the binary tree, that is, the first element of the list. If we directly delete the first element from the list, all nodes in the binary tree will be shifted accordingly (the index will change), so subsequent use of heap restoration will be easy. Very troublesome. To minimize element index changes, take the following steps:

  1. Exchange the top element and the bottom element of the heap (that is, exchange the root node and the rightmost leaf node);
  2. After the exchange is completed, delete the bottom of the heap from the list (note that because it has been exchanged, the original top element of the heap is actually deleted);
  3. Starting from the root node, perform heapization from top to bottom ;

As the name implies, the operation direction of heaping from top to bottom is opposite to that of heaping from bottom to top . We compare the value of the root node and the values ​​​​of its two child nodes, exchange the largest child node with the root node, and loop The above operation ends when the leaf node is crossed, or ends early when a node that does not need to be exchanged is encountered.

/* 元素出堆 */
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. Common applications of the heap¶

  • priority queue . The heap is often used as the preferred data structure for implementing priority queues. The time complexity of enqueueing and dequeueing operations is O(log⁡ n), and the operation of creating a queue is O(n), which are very efficient.
  • Heap sort . Given a set of data, we use it to build a heap and pop all of them in turn, then we can get an ordered sequence. Of course, heap sorting generally does not need to pop elements, but only needs to exchange the top elements of the heap to the end of the array and reduce the length of the heap every round.
  • Get the largest k elements . This is not only a classic algorithm problem, but also a common application, such as selecting the top 10 news as Weibo hot searches, selecting the top 10 sales products, etc.

3. Merge sort 

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;
    }
};

 The recursion of merge sort can also be written without 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);
        }
    }

 Extension of Merge Sort (1): Reversed pairs in an array

Jianzhi Offer 51. Reverse order pairs in an array - Leetcode

Find the reverse order pairs in the array, you can use merge sort to traverse. The most critical step is how to find the reverse order pairs across groups. That is, in reverse order, one number is in [l, mid] and the other is in [mid+1, r], and the statistics are guaranteed to be complete without omission.

Method: According to the requirements of the reverse order pair, if the previous number is greater than the latter number, then these two numbers form a reverse order pair. The group pointed to by i is in front of the group pointed to by j, and the group of i and the group of j have been sorted in ascending order. In other words, if nums[i]>nums[j], the numbers after nums[i] (until nums[mid]) are larger than nums[i], indicating that the numbers after nums[i] are greater than nums[j], the reverse order pairs across groups are found. And this way of finding can be found completely.

For example: Find the number of reverse pairs of [7,5,6,4].

By merge sort, the first round of sorting: 

 A reverse order pair [7,5], count++count=1 was found when comparing 7 and 5, and a reverse order pair was found when comparing [6,4], count++, count=2.

Second round of sorting:

When comparing 5 and 4, 5>4, that is, nums[i]>nums[j], and [5,7] has been sorted in ascending order, then it means that the numbers from 5 to mid ([7]) are all >4, The reverse order pairs found in this comparison are [5,4] and [7,4], and there are mid-i+1 in total, so count+=mid-i+1. So this time count+=1-0+1=2, count=4,

When comparing 7 and 6, nums[i]>nums[j] appears again, count+=1-1+1=1, count=5

Answer: 5

The code for counting the number of reverse pairs:

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

 The rest of the code is merge sort.

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;
    }
};

Extension of merge sort (2): important reverse order pairs

OpenJudge - M: Significant inversion pairs

describe

Given a sequence of N numbers a1, a2,...aN, the necessary and sufficient condition to define a pair (ai, aj) as an "important reverse pair" is i < j and ai > 2aj. Find the number of "important inversion pairs" in the given sequence.

enter

There are multiple test points in this question, and each test point is divided into two lines: the first line is the number N of numbers in the sequence (1 ≤ N ≤ 200000), and the second line is the sequence a1, a2 ... aN(0 ≤ a ≤ 10000000), separated by spaces. N=0 indicates the end of the input.

output

One line for each test point, output an integer, which is the number of "important reverse order pairs" in the given sequence.

sample input

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

sample output

16

Note that the judgment of important reverse order pairs cannot be written in the judgment of nums[i] and nums[j], which will cause an extra cycle and timeout.

Answer with timeout:

#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;
}

The correct way to write is to write the judgment of important reverse order pairs outside:

#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;
}

Guess you like

Origin blog.csdn.net/icecreamTong/article/details/128424838