本文记录作者刷题过程中与排序相关的题目。

一、通用排序

912. 排序数组(中等难度)

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        // 1.选择排序
        //selectSort(nums);
        // 2.冒泡排序
        //bubbleSort(nums);
        // 3.插入排序
        //insertSort(nums);
        // 4.快速排序
        //quickSort(nums,0,nums.size()-1);
        // 5.希尔排序
        //shellSort(nums);
        // 6.归并排序
        //mergeSort(nums, 0, nums.size()-1);
        // 7.堆排序
        heapSort(nums);
        return nums;
    }
    /**选择排序**/
    void selectSort(vector<int> &nums) {
        for(int i=0;i<nums.size();i++){
            // 查找未排序序列中的最值
            int minIndex = i;
            for(int j=i+1;j<nums.size();j++){
                if(nums[minIndex]>nums[j]){
                    minIndex=j;
                }
            }
            // 交换nums[i]和nums[minIndex]
            if(minIndex!=i){
                int temp=nums[minIndex];
                nums[minIndex]=nums[i];
                nums[i]=temp;
            }
        }
    }
    /**冒泡排序**/
    void bubbleSort(vector<int> &nums) {
        bool swapped;
        for(int i=nums.size()-1;i>0;i--){
            swapped = false;
            // 查找未排序序列中的最值
            for(int j=0;j<i;j++){
                if(nums[j]>nums[j+1]){
                    int temp=nums[j+1];
                    nums[j+1]=nums[j];
                    nums[j]=temp;
                    swapped=true;
                }
            }
            if (!swapped) break;
        }
    } 
    /**插入排序**/
    void insertSort(vector<int> &nums) {
        int n = nums.size();
        // 从左至右扩展已排序序列
        for (int i = 1; i < n; i++) {
            // 从右至左在已排序序列交换完成排序
            for (int j = i; j > 0; j--) {
                // 交换元素 swap(nums[j], nums[j-1])
                if(nums[j] < nums[j-1]){
                    int tmp = nums[j];
                    nums[j] = nums[j-1];
                    nums[j-1] = tmp;
                }
            }
        }
    }
    
    
    /**快速排序**/
    void quickSort(vector<int> &nums, int left, int right) {
        if (left >= right) return; 
        int first = left, last = right, key = nums[first];
        while (first < last) {
            // 从右往左找到第一个小于key的值
            while (first < last && nums[last] >= key) --last;
            nums[first] = nums[last];
            // 从左往右找到第一个大于key的值
            while (first < last && nums[first] <= key) ++first;
            nums[last] = nums[first];
        }
        nums[first] = key;
        quickSort(nums, left, first-1);
        quickSort(nums, first+1, right);
    }
    
    /**希尔排序**/
    void shellSort(vector<int> &nums) {
        int n = nums.size();
        int h = 1; 
        while (h < n/3) h = 3*h + 1;//初始增量h应该比n/3稍大,不能直接为n/3(因为必须保证h的最后一个值为1)
        for(;h >= 1;h /= 3) {  // 将数组变为h有序
            // 遍历各个组(从h开始)
            for (int i = h; i < n; i++) {
                //遍历各个组中的所有的元素(从右到左,每个组元素索引间隔h)
                for (int j = i-h; j >= 0; j -= h) {
                    if(nums[j] > nums[j+h]){
                        swap(nums[j],nums[j+h]);// 交换两个元素
                    }
                }
            }
        }
    }
    /**归并排序**/
    void mergeSort(vector<int> &nums, int left, int right){
        // 如果 left == right,表示数组只有一个元素,则不用递归排序
        if (left < right) {
            // 把大的数组分隔成两个数组
            int mid = left + (right - left) / 2;
            // 对左半部分进行排序
            mergeSort(nums, left, mid);
            // 对右半部分进行排序
            mergeSort(nums, mid + 1, right);
            // 将两部分合并后再重新排一次序
            //1、先用一个临时数组把他们合并汇总起来
            vector<int> temp(right - left + 1);
            int i = left;
            int j = mid + 1;
            int k = 0;
            //2、分别选取左右两半部分中的较小值放入临时数组
            while (i <= mid && j <= right) {
                // 谁小先放谁到临时数组
                if (nums[i] < nums[j]) {
                    temp[k++] = nums[i++];
                } else {
                    temp[k++] = nums[j++];
                }
            }
            //3、跳出循环的条件i>mid或者j>right,此时两个小数组中有一个一定没有剩余,另一个有未放入临时数组的数
            // 不知道哪个有剩余,干脆两者都判断下,有剩余直接进循环
            while(i <= mid) temp[k++] = nums[i++];
            while(j <= right) temp[k++] = nums[j++];
            // 此时原有数组的左右部分全部复制到临时数组并已排序,把临时数组复制回原数组
            for (i = 0; i < k; i++) {
                nums[left++] = temp[i];
            }
        }
    }
    /**堆排序**/
    void adjust(vector<int> &nums, int len, int parent){
        int left = 2 * parent + 1; // index的左子节点
        int right = 2 * parent + 2;// index的右子节点

        int maxIdx = parent;//假设父节点为最大值
        // 如果左子节点大于最大值,则最大值设为左子节点
        if (left < len && nums[left] > nums[maxIdx])     maxIdx = left;
        // 如果右子节点大于最大值,则最大值设为右子节点
        if (right < len && nums[right] > nums[maxIdx])     maxIdx = right;
        // 如果此时父节点已经不是最大值
        if (maxIdx != parent) {
            swap(nums[maxIdx], nums[parent]);//交换父节点和最大值,
            adjust(nums, len, maxIdx);//交换后,原来最大值的位置可能已不是其对应子树的最大值,所以要重新调整下
        }
    }
    // 堆排序
    void heapSort(vector<int> &nums){
        int len=nums.size();
        // 构建最大堆
        for(int i = (len-1) / 2 ; i >= 0; i--){
            adjust(nums, len, i);// 负责将i为父节点的子树调整为堆
        }
        // 进行堆排序
        for(int i = len - 1; i >= 1; i--){
            // 把最大堆的堆顶元素与最后一个元素交换
            swap(nums[0], nums[i]); 
            // 调整打乱的最大堆,恢复堆的特性   
            adjust(nums, i, 0);              
        }
    }
};

75. 颜色分类(中等难度)

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int n=nums.size();
        if(n<2) return;
        // 整个数组需要分成3个区间:
        int zero = 0;//[0,zero),红色元素,存放0
        int one = 0;//[zero,one),白色元素,存放1,同时负责遍历
        int two = n-1;// [one,two),篮色元素,存放2
        while (one < n) {
            // 如果当前元素==0,移动到头部
            if (nums[one] == 0&&zero<one) 
                swap(nums[one], nums[zero++]);
            
            // 如果当前元素==2,移动到表尾
            else if (nums[one] == 2&& one<two) {
                swap(nums[one], nums[two--]);
            }
            // 如果当前元素==1,跳过
            else {
                one++;
            }
        }
    }
    void quickSort(vector<int>& nums, int left,int right){
        if (left >= right) return; 
        int first=left,last=right,key=nums[first];
        // 开始分区
        while(first<last){
            // 从右到左找第一个比key小的数
            while (first<last&&nums[last]>=key)last--; 
            nums[first]=nums[last];
            // 从左到右找第一个比key大的数
            while (first<last&&nums[first]<=key )first++;
            nums[last]=nums[first];
        }
        nums[first]=key;//基准值归位
        // 结束分区
        quickSort(nums,left,first-1);
        quickSort(nums,first+1,right);
    }
};

179. 最大数(中等难度)

class Solution {
public:
    static bool cmp(int a,int b){
        string sa = to_string(a);
        string sb = to_string(b);
        return sa+sb>sb+sa;//难点,首字母相同的树,C++默认比较第二个字母
    }

    string largestNumber(vector<int>& nums) {
        string result;
        sort(nums.begin(),nums.end(),cmp);//将每个数排序
        if(nums[0]==0) return "0";// 排除特殊情况,第1个数字是0
        // 将数组内的数拼接起来
        for(auto num:nums) result+=to_string(num);
        return result;
    }
};

252. 会议室(简单难度)

class Solution {
public:
    static bool cmp(pair<int, int>&a,pair<int, int>&b){
        // 如果两个时间点相同,则结束时间在前
        if(a.first==b.first) return a.second<b.second;
        // 如果两个时间点不同,则小的时间在前
        else return a.first<b.first;
    }
    bool canAttendMeetings(vector<vector<int>>& intervals) {
        int n = intervals.size();
        if (n < 2) return true;
        // 将所有的开始时间和结束时间加入数组
        vector<pair<int, int>>  times;
        for (auto & inter : intervals) {
            times.emplace_back(make_pair(inter[0],1));//会议开始,会议室+1
            times.emplace_back(make_pair(inter[1],-1));//会议结束,会议室-1
        }
        // 将所有的时间点排序
        sort(times.begin(), times.end(), cmp); 
        int works = 0, res = 1;                             
        for (auto & t : times) {
            works += t.second;// 此时开会的会议室数量
            res = max(res, works);// 记录最多数量
        }
        return res==1;
    }
};

253. 会议室 II(中等难度)

class Solution {
public:
    /*按这道题其实就是操作系统课程中,
    计算确定运行时间的多线程占用资源至少数量的问题,
    创建一个序列记录每个会议的起止时间,
    并用1和-1分别标记这个时间是开始还是结束,
    这样就能确定某个时刻有几个会议室处于工作中*/
    static bool cmp(pair<int, int>&a,pair<int, int>&b){
        // 如果两个时间点相同,则结束时间在前
        if(a.first==b.first) return a.second<b.second;
        // 如果两个时间点不同,则小的时间在前
        else return a.first<b.first;
    }
    int minMeetingRooms(vector<vector<int>>& intervals) {
        int n = intervals.size();
        if (n < 2) return n;
        // 将所有的开始时间和结束时间加入数组
        vector<pair<int, int>>  times;
        for (auto & inter : intervals) {
            times.emplace_back(make_pair(inter[0],1));//会议开始,会议室+1
            times.emplace_back(make_pair(inter[1],-1));//会议结束,会议室-1
        }
        // 将所有的时间点排序
        sort(times.begin(), times.end(), cmp); 
        int works = 0, res = 1;                             
        for (auto & t : times) {
            works += t.second;// 此时开会的会议室数量
            res = max(res, works);// 记录最多数量
        }
        return res;
    }
};

二、插入排序

147. 对链表进行插入排序

class Solution {
public:
    // 从小到大排序,难点:插入算法的思想,搜索插入位置
    ListNode* insertionSortList(ListNode* head) {
        if(head==nullptr) return nullptr;
        // 新建虚拟头结点
        ListNode* myHead=new ListNode(0);
        myHead->next=head;
        // 双指针进行插入排序
        ListNode* lastSorted=head;//最后一个排序好的节点
        ListNode* cur=head->next;// 当前遍历到的节点
        while(cur){
            // 如果当前节点值>最后一个排序好的节点,直接+1
            if(cur->val>=lastSorted->val){
                lastSorted=lastSorted->next;
            }
            // 如果当前节点值<最后一个排序好的节点,从头遍历寻找插入位置
            else{
                ListNode* prev = myHead;//从虚拟头结点开始
                // 遍历找到第一个prev->next的值大于cur的值
                while (prev->next->val <= cur->val) {
                    prev = prev->next;
                }
                // 保存cur的后一个元素
                lastSorted->next = cur->next;//排序好的节点+1
                // 在prev后面插入cur
                cur->next = prev->next;//当前节点
                prev->next = cur;
            }
            cur = lastSorted->next;
        }
        return myHead->next;
    }
};

三、快速排序

215. 数组中的第K个最大元素(中等难度)

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int len = nums.size();
        //从大到小排序,第k大就是第k-1个索引,返回第k-1个索引的值
        return quickFind(nums,k-1,0,len-1);
    }
    // 快排法
    int quickFind(vector<int>& nums, int k,int left,int right){
        int index = partition(nums,left,right);//开始分区
        // 结束分区
        if(index==k) return nums[index];//first是基准值索引,表示第first个
        else if(index>k)  return quickFind(nums, k,left,index-1);
        else return quickFind(nums, k,index+1,right);
    }
    // 分区函数
    int partition (vector<int> &nums, int first, int last) {
        int key = nums[first];//默认第一个为基准值
        while (first < last) {
            // 从右往左找到第一个小于key的值
            while (first < last && nums[last] <= key) --last;
            nums[first] = nums[last];
            // 从左往右找到第一个大于key的值
            while (first < last && nums[first] >= key) ++first;
            nums[last] = nums[first];
        }
        nums[first] = key;// 基准值归位
        return first;// 返回索引
    } 
};

四、归并排序

88. 合并两个有序数组

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        vector<int> temp(m+n);//临时数组
        int i=0,j=0,k=0;//双指针 //从小到大
        while(i<m&&j<n){
            if(nums1[i]<nums2[j]) temp[k++]=nums1[i++];
            else temp[k++]=nums2[j++];
        }
        while(i<m) temp[k++]=nums1[i++];
        while(j<n) temp[k++]=nums2[j++];
        for(int i=0;i<m+n;i++) nums1[i]=temp[i];
    }
};

21. 合并两个有序链表

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* result = new ListNode(-1); // 结果链表,默认第一个为虚拟头结点
        ListNode* p = result;// 遍历指针,该指针会变化
        while(list1!=NULL&&list2!=NULL){
            if(list1->val<=list2->val){
                p->next=list1;
                list1=list1->next;
                p=p->next;
            }
            else{
                p->next=list2;
                list2=list2->next;
                p=p->next;
            }
        }
        while(list1!=NULL){
            p->next=list1;
            list1=list1->next;
            p=p->next;
        }
        while(list2!=NULL){
            p->next=list2;
            list2=list2->next;
            p=p->next;
        }
        return result->next;
    }
};

剑指 Offer 51. 数组中的逆序对

class Solution {
public:
    /*逆序对:数组中的长度为2且第1个元素大于第2个元素的子序列
    该题可以利用归并排序,归并排序先划分出若干个有序数组,然后合并
    合并时,需要比较有序前半段nums[i]和有序后半段的nums[j]:
    如果nums[i]<=nums[j]:说明当前位置没有逆序;
    如果nums[i] > nums[j]: 说明在有序的前半段中,位置i的都已经比nums[j]大,
    则前半段中位置i及其后面的数字一定比nums[j]大,大的数是区间[i,mid],则
    每次累加mid-i+1
    该题的答案只在归并排序中加了一个res+=mid-i+1*/
    int res=0;
    int reversePairs(vector<int>& nums) {
        mergeSort(nums,0,nums.size()-1);
        return res;
    }
    void merge(vector<int>& nums,int left,int mid,int right){
        //1、先用一个临时数组把他们合并汇总起来
        vector<int> temp(right - left + 1);
        int i = left;
        int j = mid + 1;
        int k = 0;
        //2、分别选取左右两半部分中的较小值放入临时数组
        while (i <= mid && j <= right) {
            // 谁小先放谁到临时数组(注意必须等号)
            if (nums[i]<=nums[j]) temp[k++]=nums[i++];
            else {
                res+=mid-i+1;// 本题在归并排序算法添加的唯一一行
                temp[k++]=nums[j++];
            }
        }
        //3、跳出循环的条件i>mid或者j>right,
        //此时两个小数组中有一个一定没有剩余,另一个有未放入临时数组的数
        // 不知道哪个有剩余,干脆两者都判断下,有剩余直接进循环
        while(i<=mid) temp[k++]=nums[i++];
        while(j<=right) temp[k++]=nums[j++];
        // 此时原有数组的左右部分全部复制到临时数组并已排序,把临时数组复制回原数组
        for (i = 0; i < k; i++) nums[left++] = temp[i];
    }
    void mergeSort(vector<int> &nums, int left, int right){
        if(left>=right) return;// 表示数组只有一个元素,则不用递归排序
        // 把大的数组分隔成两个数组
        int mid = left+(right-left) / 2;
        // 对左半部分进行排序
        mergeSort(nums, left, mid);
        // 对右半部分进行排序
        mergeSort(nums, mid +1, right);
        // 将两部分合并后再重新排一次序
        merge(nums, left, mid,right);
    }
};

五、堆排序

​ 堆排序(heap-sort)是利用一种叫做堆(heap)的数据结构所设计的排序算法。堆本质上就是用数组实现的二叉树,所以也叫做二叉堆。堆有一个性质,叫做堆有序。根据这种堆有序的性质,堆分为2种,最大堆(大顶堆)和最小堆(小顶堆)。在最大堆中,父节点的值都比每一个子节点的值要大;在最小堆中,父节点的值都比每一个子节点的值要小。(注意和二叉搜索树的区别,二叉搜索树是左子树的值比父节点的值小,右子树的值比父节点的值大)。下图是一个最大堆的内部数据的逻辑结构,根据这种特点,给定任意一个数所在的索引index,我们就可以知道它的父节点索引或者左右子节点的索引。

1、算法思路

​ 假设从小到大排序,堆排序的基本思路是先将整个无序数组构造成一个最大堆,那么最大堆的根节点一定就是全局最大的数,将其取出来放入已排序序列;删除掉根节点的最大堆在重新构建一个最大堆,从而又找到了一个局部最大值,接着将其放入已排序序列,当无序序列中构建最大堆的数只剩下一个时,排序也就完成。过程如下:

2、编码思路

​ 在上述思路分析中,可知将一个无序序列转换为堆是最关键的步骤。最大堆是一个父节点都大于子节点的二叉树,在编码上可以采取递归的方式来实现。

​ 假设我们有一个函数adjust(),其功能就是负责将i为根节点的子树调整为堆有序,即满足最大堆或最小堆,那么我们借助这个adjust就可以写出如下堆排序的代码:

​ 现在我们的任务就是写出adjust函数,其也利用了二叉树递归的思路,只要保证每个小子树中父节点比孩子节点都大或者都小即可,如下:

3、性能分析

​ 先给出堆排序的性能分析结论:

​ 此时我们已经完成了整个堆排序的任务,最重要的是adjust的实现,读者要多默写几遍。

​ 为了帮助读者学习该算法,笔者给出了自己写本文的参考资料和代码。本文的归并排序的代码如下:

//调整为堆有序,len是调整的序列长度,parent是堆对应的二叉树的根节点索引
void adjust(vector<int> &nums, int len, int parent){
    int left = 2 * parent + 1; // parent的左子节点
    int right = 2 * parent + 2;// parent的右子节点

    int maxIdx = parent;//假设父节点为最大值
    // 如果左子节点大于最大值,则最大值设为左子节点
    if (left < len && nums[left] > nums[maxIdx])     maxIdx = left;
    // 如果右子节点大于最大值,则最大值设为右子节点
    if (right < len && nums[right] > nums[maxIdx])     maxIdx = right;
    // 如果此时父节点已经不是最大值
    if (maxIdx != parent) {
        swap(nums[maxIdx], nums[parent]);//交换父节点和最大值,
        adjust(nums, len, maxIdx);//交换后,原来最大值的位置可能已不是其对应子树的最大值,所以要重新调整下
    }
}
/**堆排序**/
void heapSort(vector<int> &nums){
    int len=nums.size();
    // 构建最大堆(从右向左依次传入父节点的索引i,最右侧的父节点索引一定是(len-1)/2)
    for(int i = (len-1) / 2 ; i >= 0; i--){
        adjust(nums, len, i);// 负责将i为根节点的子树调整为堆
    }
    // 进行堆排序
    for(int i = len - 1; i >= 1; i--){
        // 把最大堆的堆顶元素与最后一个元素交换
        swap(nums[0], nums[i]); 
        // 调整剩余的打乱的数为最大堆,其根节点索引始终是0,长度为i(i=1时表示排序完成)
        adjust(nums, i, 0);              
    }
}

六、桶排序/计数排序

1365. 有多少小于当前数字的数字(简单难度)

方法一:

class Solution {
public:
    /* [8,1,2,2,3]排序后就是[1,2,2,3,8],其索引就是[0,1,2,3,4]
    最终想要[0,1,1,3,4],发现除了重复元素其他就是其所在索引*/
    vector<int> smallerNumbersThanCurrent(vector<int>& nums) {
        vector<pair<int, int>> data;// 数字-索引
        int n = nums.size();
        for (int i = 0; i < n; i++) data.emplace_back(nums[i], i);
        // 按照数字从小到大排序,排序后
        sort(data.begin(), data.end());
        // 预先建立一个n个元素的数组
        vector<int> res(n, 0);
        // 遍历索引
        int prev = -1;
        for (int i = 0; i < n; i++) {
            if (prev==-1|| data[i].first != data[i - 1].first) {
                prev=i;
            }
            res[data[i].second] = prev;// 对应索引
        }
        return res;
    }
};

七、奇偶排序

八、拓扑排序

二、堆排序