Leetcode_排序

这篇博客详细介绍了多种数组处理的算法,包括寻找重复数字、根据身高重建队列、移动零、寻找数组中的第K个最大元素、下一个排列、合并区间、二维数组查找、数组中的逆序对以及最小数字排列。文章通过Java代码展示了如何实现这些算法,同时分析了各自的时间和空间复杂度。
摘要由CSDN通过智能技术生成

*数组中重复的数字

1 题目要求

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。

请找出数组中任意一个重复的数字。

2 解题(Java)

2.1 解题思路

  1. 注意到数组中的数字都在 0 ~ n-1 的范围内。如果将数字i放置在下标为i的位置,由于数组中有重复的数字,有些位置可能存在多个数字,同时可能有些位置没有数字;
  2. 从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果它和第m个数字相等,就找到了一个重复的数字(该数字在下标为i和m的位置都出现了);如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字;

2.2 代码

class Solution {
    public int findRepeatNumber(int[] nums) {
        int i = 0;
        while (i < nums.length) {
            if (nums[i] == i) {
                i++;
                continue;
            } else if (nums[nums[i]] == nums[i]) {
                return nums[i];
            } else {
                int tmp = nums[i];
                nums[i] = nums[tmp];
                nums[tmp] = tmp;
            }
        }
        return -1;
    }
}

3 复杂度分析

  • 时间复杂度O(N):遍历数组使用 O(N) ,每轮遍历的判断和交换操作使用 O(1);
  • 空间复杂度O(1)

*根据身高重建队列

1 题目描述

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

提示

  • 1 <= people.length <= 2000
  • 0 <= hi <= 106
  • 0 <= ki < people.length
  • 题目数据确保队列可以被重建

2 解题(Java)

排序

/**
 * 解题思路:先排序再插入
 * 1.排序规则:按照先H高度降序,K个数升序排序
 * 2.遍历排序后的数组,根据K插入到K的位置上
 *  * 核心思想:高个子先站好位,矮个子插入到K位置上,前面肯定有K个高个子,矮个子再插到前面也满足K的要求
*/
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        // [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
        // 再一个一个插入。
        // [7,0]
        // [7,0], [7,1]
        // [7,0], [6,1], [7,1]
        // [5,0], [7,0], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [7,1]
        // [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
        Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0]);

        List<int[]> res = new LinkedList<>();
        for (int[] p : people) {
            res.add(p[1], p);
        }

        return res.toArray(new int[][]{});
    }
}

3 复杂性分析

  • 时间复杂度:O(nlogn),其中 n 是数组people 的长度。需要O(nlogn) 的时间进行排序,随后需要 O(n)时间遍历每一个人插入列表里,因此总时间复杂度为 O(nlogn);
  • 空间复杂度:O(logn),即为排序需要使用的栈空间;

*移动零

1 题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。

2 解题(Java)

快排思想

class Solution {
    public void moveZeroes(int[] nums) {
        if (nums == null) return;
        int left = 0;
        for (int right=0; right<nums.length; right++) {
            if (nums[right] != 0) {
                int temp = nums[right];
                nums[right] = nums[left];
                nums[left++] = temp;
            }
        }
    }
}

3 复杂性分析

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

*数组中的第K个最大元素

1 题目描述

在未排序的数组中找到第 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

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

2 解题(Java)

解题思路

  1. 可以用快速排序来解决这个问题,先对原数组排序,再返回倒数第 k 个位置,这样平均时间复杂度是 O(nlogn),但其实可以做得更快;

  2. 对数组 a[l⋯r] 做快速排序的过程如下:

    • 分解: 将数组 a[l⋯r] 「划分」成两个子数组 a[l⋯q−1]、a[q+1⋯r],使得 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分;
    • 解决: 通过递归调用快速排序,对子数组 a[l⋯q−1] 和 a[q+1⋯r] 进行排序;
    • 合并: 因为子数组都是原址排序的,所以不需要进行合并操作,a[l⋯r] 已经有序;
    • 上文中提到的 「划分」 过程是:从子数组 a[l⋯r] 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q;
  3. 由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案;

  4. 因此我们可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分,如果划分得到的 q 正好就是我们需要的下标,就直接返回 a[q];否则,如果 q 比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法;

  5. 我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为 n 的问题我们都划分成 1 和 n−1,每次递归的时候又向 n−1 的集合中递归,这种情况是最坏的,时间代价是 O(n 2)。我们可以引入随机化来加速这个过程,它的时间代价的期望是 O(n),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」;

代码

class Solution {
    Random random = new Random();

    public int findKthLargest(int[] nums, int k) {
        return quickSelect(nums, 0, nums.length - 1, nums.length - k);
    }

    public int quickSelect(int[] a, int l, int r, int index) {
        int q = randomPartition(a, l, r);
        if (q == index) {
            return a[q];
        } else {
            return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
        }
    }

    public int randomPartition(int[] a, int l, int r) {
        int i = random.nextInt(r - l + 1) + l;
        swap(a, i, l);
        return partition(a, l, r);
    }

    public int partition(int[] a, int l, int r) {
        int x = a[l], left = l, right = r;
        while (left < right) {
            while (left < right && a[right] >= x) {
                right--;
            }
            while (left < right && a[left] <= x) {
                left++;
            }
            swap(a, left, right);
        }
        swap(a, l, left);

        return left;
    }

    public void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

3 复杂性分析

  • 时间复杂度O(n):如上文所述,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」;
  • 空间复杂度O(logn):递归使用栈空间的空间代价的期望为 O(logn);

*下一个排列

1 题目描述

实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须 原地 修改,只允许使用额外常数空间。

示例 1

输入:nums = [1,2,3]
输出:[1,3,2]

示例 2

输入:nums = [3,2,1]
输出:[1,2,3]

示例 3

输入:nums = [1,1,5]
输出:[1,5,1]

示例 4

输入:nums = [1]
输出:[1]

提示

1 <= nums.length <= 100
0 <= nums[i] <= 100

2 解题(Java)

算法流程

  1. 首先从后向前查找第一个顺序对 (i,i+1),满足 a[i] < a[i+1]。这样「较小数」即为 a[i]。此时 [i+1,n) 必然是下降序列;
  2. 如果找到了顺序对,那么在区间 [i+1,n)中从后向前查找第一个元素 j满足 a[i] < a[j]。这样「较大数」即为 a[j];
  3. 交换 a[i] 与 a[j],此时可以证明区间 [i+1,n) 必为降序。我们可以直接使用双指针反转区间 [i+1,n)使其变为升序,而无需对该区间进行排序;
  4. 如果在步骤 1 找不到顺序对,说明当前序列已经是一个降序序列,即最大的序列,我们直接跳过步骤 2 执行步骤 3,即可得到最小的升序序列;

代码

class Solution {
    public void nextPermutation(int[] nums) {
        if (nums.length <= 1) return;
        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }
        if (i >= 0) {
            int j = nums.length - 1;
            while (nums[i] >= nums[j]) {
                j--;
            }
            swap(nums, i, j);
        }
        reverse(nums, i + 1);
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    public void reverse(int[] nums, int start) {
        int left = start, right = nums.length - 1;
        while (left < right) {
            swap(nums, left, right);
            left++;
            right--;
        }
    }
}

3 复杂性分析

  • 时间复杂度O(N):其中 N 为给定序列的长度,我们至多只需要扫描两次序列,以及进行一次反转操作;
  • 空间复杂度O(1):只需要常数的空间存放若干变量;

*合并区间

1 题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

示例 1

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示

  • 1 <= intervals.length <= 10 ^ 4
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 10 ^ 4

2 解题(Java)

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0) {
            return new int[0][0];
        }
        Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]);
        List<int[]> merged = new ArrayList<>();
        for (int i=0; i<intervals.length; i++) {
            int L = intervals[i][0], R = intervals[i][1];
            // 如果列表为空,或者当前区间与上一区间不重合,直接添加
            if (merged.size() == 0 || merged.get(merged.size() -1)[1] < L) {
                merged.add(new int[]{L, R});
            }
            // 否则与上一区间合并
            else {
                merged.get(merged.size()-1)[1] = Math.max(merged.get(merged.size()-1)[1], R);
            }
        }
        return merged.toArray(new int[][]{});
    }
}

3 复杂性分析

  • 时间复杂度O(nlogn):其中 n 为区间的数量。除去排序的开销,只需加一次线性扫描,所以主要的时间开销是排序的 O(nlogn);
  • 空间复杂度O(logn):除了存储答案之外,使用的额外空间即为排序所需要的空间复杂度O(logn);

*二维数组中的查找

1 题目描述

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有二维数组如下:

[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]

给定 target = 5,返回 true。

给定 target = 20,返回 false。

2 解题(Java)

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        if (matrix.length==0 || matrix[0].length==0) return false;
        int rows = matrix.length, columns = matrix[0].length;
        int row = 0, column = columns-1;
        while(row <= rows-1 && column >= 0) {
            if (matrix[row][column] > target) column--;
            else if (matrix[row][column] < target) row++;
            else return true;
        }
        return false;
    }
}

3 复杂性分析

  • 时间复杂度:O(n+m)。访问到的下标的行最多增加 n 次,列最多减少 m 次,因此循环体最多执行 n + m 次;
  • 空间复杂度:O(1);

*数组中的逆序对

1 题目描述

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

示例 1:

输入: [7,5,6,4]
输出: 5

限制:

0 <= 数组长度 <= 50000

2 解题(Java)

归并排序法,并的过程中统计个数:

class Solution {
    int count;
    int[] assist;
    public int reversePairs(int[] nums) {
        int len = nums.length;
        assist = new int[len];
        sort(nums, 0, len - 1);
        return count;
    }
    public void sort(int[] nums, int left, int right) {
        if (left >= right) return;
        int mid = (left + right) / 2;
        sort(nums, left, mid);
        sort(nums, mid + 1, right);
        mergeAndCount(nums, left, mid, right);
    }
    public void mergeAndCount(int[] nums, int left, int mid, int right) {
        int p1 = left, p2 = mid + 1;
        int index = left;
        while (p1 <= mid && p2 <= right) {
            if (nums[p1] > nums[p2]) {
                assist[index++] = nums[p2++];
                count += mid - p1 + 1;
            } else {
                assist[index++] = nums[p1++];
            }
        }
        while (p1 <= mid) assist[index++] = nums[p1++];
        while (p2 <= right) assist[index++] = nums[p2++];

        for (index = left; index <= right; index++) {
            nums[index] = assist[index];
        }
    }
}

3 复杂性分析

  • 时间复杂度O(NlogN):同归并排序O(NlogN);
  • 空间复杂度O(N):同归并排序 O(N),因为归并排序需要用到一个临时数组;

把数组排成最小的数

1 题目描述

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

示例 1:

输入: [10,2]
输出: “102”

示例 2:

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

提示:

0 < nums.length <= 100

说明:

  • 输出结果可能非常大,所以你需要返回一个字符串而不是整数;
  • 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0;

2 解题(Java)

2.1 解题思路

  1. 此题求拼接起来的 “最小数字” ,本质上是一个排序问题;
  2. 排序判断规则: 设 nums 任意两数字的字符串格式 x 和 y ,则
    • 若拼接字符串 x + y > y + x,则 x > y;
    • 反之,若 x + y < y + x,则 x < y;
  3. 根据以上规则,套用任何排序方法对 nums 执行排序即可;

在这里插入图片描述

2.2 算法流程

  1. 初始化: 字符串列表 strs ,保存各数字的字符串格式;
  2. 列表排序: 应用以上 “排序判断规则” ,对 strs 执行排序;
  3. 返回值: 拼接 strs 中的所有字符串,并返回;

2.3 代码(自定义快速排序)

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for (int i=0; i<nums.length; i++) {
            strs[i] = String.valueOf(nums[i]);
        }
        fastSort(strs, 0, strs.length - 1);
        StringBuilder res = new StringBuilder();
        for (String s : strs) {
            res.append(s);
        }
        return res.toString();
    }
    void fastSort(String[] strs, int lo, int hi) {
        if (lo >= hi) return;
        int left = lo, right = hi;
        while (left < right) {
            while (left < right && (strs[right] + strs[lo]).compareTo(strs[lo] + strs[right]) >= 0) {
                right--;
            }
            while (left < right && (strs[left] + strs[lo]).compareTo(strs[lo] + strs[left]) <= 0) {
                left++;
            }
            String tmp = strs[left];
            strs[left] = strs[right];
            strs[right] = tmp;
        }
        String tmp = strs[left];
        strs[left] = strs[lo];
        strs[lo] = tmp;
        fastSort(strs, lo, left - 1);
        fastSort(strs, left + 1, hi);
    }
}

2.4 代码(内置函数排序)

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for (int i=0; i<nums.length; i++) {
            strs[i] = String.valueOf(nums[i]);
        }
        Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
        StringBuilder res = new StringBuilder();
        for (String s : strs) {
            res.append(s);
        }
        return res.toString();
    }
}

3 复杂性分析

  • 时间复杂度 O(NlogN) : 使用快排或内置函数的平均时间复杂度为 O(NlogN) ,最差为 O(N^2);
  • 空间复杂度 O(N) : 字符串列表 strs 占用线性大小的额外空间;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值