算法(十九)数组之排序

leetcode

[hot] 215. 数组中的第K个最大元素

题目

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

题解

堆排序解决问题。示例代码如下所示:
在这里插入图片描述

class Solution {
public:
    void max_heapify(vector<int>& nums, int heap_size, int i) {
        int left = i * 2 + 1;
        int right = i * 2 + 2;
        int largest = i;
        // 不是i,是left,下同
        if (left < heap_size && nums[left] > nums[largest]) {
            largest = left;
        }
        if (right < heap_size && nums[right] > nums[largest]) {
            largest = right;
        }
        if (largest != i) {
            // 记得交换
            swap(nums[i], nums[largest]);
            max_heapify(nums, heap_size, largest);
        }
    }

    void build_max_heap(vector<int>& nums, int heap_size) {
        for (int i = heap_size / 2; i >= 0; --i) {
            max_heapify(nums, heap_size, i);
        }
    }

    int findKthLargest(vector<int>& nums, int k) {
        int length = nums.size();
        int result = 0;
        int heap_size = length;
        build_max_heap(nums, heap_size); // 第一个回溯构建最大堆
        for (int i = length - 1; i >= length - k + 1; --i) { // 第二个回溯找到目标元素
            swap(nums[0], nums[i]);
            --heap_size;
            // 从0开始重新维护大根堆
            max_heapify(nums, heap_size, 0);
        }

        return nums[0];
    }
};

复杂度

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),堆排序的时间复杂度
空间复杂度: O ( l o g n ) O(logn) O(logn),堆排序的空间复杂度

题解2

快排解决问题,示例代码如下:

// 快排版本
class Solution {
public:
    int quickselect(vector<int> &nums, int l, int r, int k) {
        if (l == r) {
            return nums[k];
        }
           
        int partition = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do {
                i++;
            } while (nums[i] < partition);
            do {
                j--;
            }  while (nums[j] > partition);
            if (i < j) {
                swap(nums[i], nums[j]);
            }
                
        }
        if (k <= j) {
            return quickselect(nums, l, j, k);
        } else {
            return quickselect(nums, j + 1, r, k);
        }
    }

    int findKthLargest(vector<int> &nums, int k) {
        int n = nums.size();
        // 相当于是找到数组中第n - k个元素(下标从0开始)
        return quickselect(nums, 0, n - 1, n - k);
        // 如果找第k小的元素,如下
        // return quickselect(nums, 0, n - 1, k - 1);
    }
};

复杂度

时间: O ( n ) O(n) O(n),等比数列求和公式, n + n / 2 + n / 4 + . . . + n / n = n − 1 / 2 1 − 1 / 2 = 2 n − 1 n + n/2 + n/4 + ... + n/n = \frac{n-1/2}{1-1/2}=2n-1 n+n/2+n/4+...+n/n=11/2n1/2=2n1
空间: O ( l o g n ) O(logn) O(logn)

1122. 数组的相对排序

题目

给你两个数组,arr1 和 arr2,arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中
对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

示例:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]
提示:
1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2 中的元素 arr2[i] 各不相同
arr2 中的每个元素 arr2[i] 都出现在 arr1 中

题解

类似计数排序得到最终结果,示例代码如下所示:

class Solution {
public:
    vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
        map<int, int> freq;
        for (auto& elem: arr1) {
            if (freq.count(elem) > 0) {
                ++freq[elem];
            } else {
                freq[elem] = 1;
            }
        }

        vector<int> ans;
        for (auto& elem: arr2) {
            for (int j = 0; j < freq[elem]; ++j) {
                ans.emplace_back(elem);
            }
            freq[elem] = 0; // 避免arr2的元素被重复放入ans中
        }

        for (auto& elem: freq) {
            auto& key = elem.first;
            auto& value = elem.second;
            for (int i = 0; i < value; ++i) {
                ans.emplace_back(key);
            }
        }

        return ans;
    }
};

复杂度

时间复杂度: O ( m l o g m + n ) O(mlogm + n) O(mlogm+n),m为arr1的长度,n为arr2的长度
空间复杂度: O ( m ) O(m) O(m)

题解2

把map换为数组,示例代码如下所示:

#include <numeric>
class Solution {
public:
    vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
        int upper = *max_element(arr1.begin(), arr1.end());
        vector<int> frequency(upper + 1);
        for (int x: arr1) {
            ++frequency[x];
        }
        vector<int> ans;
        for (int x: arr2) {
            for (int i = 0; i < frequency[x]; ++i) {
                ans.push_back(x);
            }
            frequency[x] = 0;
        }
        for (int x = 0; x <= upper; ++x) {
            for (int i = 0; i < frequency[x]; ++i) {
                ans.push_back(x);
            }
        }
        return ans;
    }
};

复杂度

时间: O ( m + n + u p p e r ) O(m + n + upper) O(m+n+upper),upper为数组元素最大取值
空间: O ( u p p e r ) O(upper) O(upper)

剑指offer

40. 最小的k个数

题目

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

题解 快速排序思想

示例代码如下所示:

class Solution {
public:
    int partition(vector<int>& arr, int start, int end) {
        int length = arr.size();
        if (start < 0 || end >= length || start > end) {
            return -1;
        }

        int x = arr[end];
        int i = start - 1;
        for (int j = start; j < end; ++j) {
            if (arr[j] <= x) {
                swap(arr[j], arr[++i]);
            }
        }

        swap(arr[i + 1], arr[end]);

        return i + 1;
    }

    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        int length = arr.size();
        int start = 0;
        int end = length - 1;
        int index = partition(arr, start, end);
        while (index != k - 1) {
            if (index > k - 1) {
                end = index - 1;
                index = partition(arr, start, end);
            } else {
                start = index + 1;
                index = partition(arr, start, end);
            }
        }

        vector<int> res;
        res.assign(arr.begin(), arr.begin() + k);

        return res;
    }
};

复杂度

时间复杂度:理想情况下,每次遍历过程中只需要一半元素,因为我们是要找下标为k的元素,第一次切分的时候需要遍历整个数组 (0 ~ n) 找到了下标是 j 的元素,假如 k 比 j 小的话,那么我们下次切分只要遍历数组 (0~k-1)的元素就行啦,反之如果 k 比 j 大的话,那下次切分只要遍历数组 (k+1~n) 的元素就行啦,总之可以看作每次调用 partition 遍历的元素数目都是上一次遍历的 1/2,因此时间复杂度是 n + n / 2 + n / 4 + . . . + n / n = n ∗ ( 2 − 1 / ( 2 n ) ) = n n+ n/2 + n/4 + ... + n/n = n*(2-1/(2^n)) = n n+n/2+n/4+...+n/n=n(21/(2n))=n, 因此时间复杂度是 O ( n ) O(n) O(n)

空间复杂度: O ( k ) O(k) O(k),最后存储结果的数组的长度为k。

51. 数组中的逆序对

题目

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:
输入: [7,5,6,4]
输出: 5

题解

归并排序过程中记录待合并的前面数组比后面数组大的情况,示例代码如下所示:

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int length = nums.size();
        vector<int> tmp(length);
        return m_s(nums, tmp, 0, length - 1);
    }

    int m_s(vector<int>& nums, vector<int>& tmp, int l, int r) {
        if (l >= r) {
            return 0;
        }

        int mid = (l + r) / 2;
        int res = m_s(nums, tmp, l, mid) + m_s(nums, tmp, mid + 1, r);
        int i = l;
        int j = mid + 1;
        int pos = l;

        while (i <= mid && j <= r) {
            // i开始移动时,需要记录的是上一次贡献的逆序对数
            if (nums[i] <= nums[j]) {
                tmp[pos] = nums[i++];
                res += j - (mid + 1);
            } else {
                tmp[pos] = nums[j++];
            }
            ++pos;
        }

        for (int k = i; k <= mid; ++k) {
            tmp[pos++] = nums[k];
            res += j - (mid + 1);
        }

        for (int k = j; k <= r; ++k) {
            tmp[pos++] = nums[k];
        }

        copy(tmp.begin() + l, tmp.begin() + r + 1, nums.begin() + l);

        return res;
    }
};

复杂度

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)归并排序时间复杂度
空间复杂度: O ( n ) O(n) O(n),辅助空间tmp

45. 把数组排成最小的数

题目

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: "102"
示例 2:

输入: [3,30,34,5,9]
输出: "3033459"

题解

此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y,则规定 排序判断规则 为:

若拼接字符串 x + y > y + x,则 x “大于” y ;
反之,若 x + y < y + x,则 x “小于” y;

用上述规则进行排序即可得到结果,示例代码如下所示:

class Solution {
public:
    void q_s(vector<string>& nums, int l, int r) {
        if (l >= r) {
            return;
        }
        int i = l;
        int j = r;
        while (i < j) {
        	// 必须先右后左,这样才能保证while循环跳出后,
        	// [l, i]之间的元素都是小于等于nums[l],因为后面需要swap
            while (i < j && nums[j] + nums[l] >= nums[l] + nums[j]) {
                --j;
            }
            while (i < j && nums[i] + nums[l] <= nums[l] + nums[i]) {
                ++i;
            }
            swap(nums[i], nums[j]);
        }
        swap(nums[i], nums[l]);
        q_s(nums, l, i - 1);
        q_s(nums, i + 1, r);
    }

    string minNumber(vector<int>& nums) {
        string result;
        int length = nums.size();
        if (length == 0) {
            return result;
        }

        vector<string> tmp;
        for (auto& num: nums) {
            tmp.emplace_back(to_string(num));
        }

        q_s(tmp, 0, length - 1);
        for (auto& str: tmp) {
            result.append(str);
        }

        return result;
    }
};

复杂度

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),快排时间复杂度
空间复杂度: O ( n ) O(n) O(n),字符串数组辅助空间大小

其他

10. 合并排序的数组

题目

给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。初始化 A 和 B 的元素数量分别为 m 和 n。

示例:
输入:
A = [1,2,3,0,0,0], m = 3
B = [2,5,6],       n = 3

输出: [1,2,2,3,5,6]

题解

从后向前赋值,这样能够保证不占用额外的空间复杂度。示例代码如下所示:

class Solution {
public:
    void merge(vector<int>& A, int m, vector<int>& B, int n) {
        int pa = m - 1;
        int pb = n - 1;
        int tail = m + n - 1;
        int cur;
        while (pa >= 0 || pb >= 0) {
            if (pa == -1) {
                cur = B[pb--];
            } else if (pb == -1) {
                cur = A[pa--];
            } else if (A[pa] > B[pb]) {
                cur = A[pa--];
            } else {
                cur = B[pb--];
            }
            A[tail--] = cur;
        }
    }
};

复杂度

时间复杂度: O ( m + n ) O(m + n) O(m+n)
空间复杂度: O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值