LeetCode每日一题打卡存档(1116-1122)

455 阅读12分钟

1116 | 根据身高重建队列

假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对 (h, k) 表示,其中 h 是这个人的身高,k 是应该排在这个人前面且身高大于或等于 h 的人数。 例如:[5,2] 表示前面应该有 2 个身高大于等于 5 的人,而 [5,0] 表示前面不应该存在身高大于等于 5 的人。

编写一个算法,根据每个人的身高 h 重建这个队列,使之满足每个整数对 (h, k) 中对人数 k 的要求。

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

提示:
总人数少于 1100 人。

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/qu… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

重建队列就是按题目要求的规则对数组进行排序,规则中同时规定了 h 和 k 的值,所以只有唯一正确答案。按照题目规则,主要是按 h 排序,但 k 又会对顺序进行调整,如果使用交换之类的方式,每次交换都要重新计算两个 k 的值,所以不太能使用需要交换的排序思路。选择排序可能是一个合适的选择。

h 和 k 是互相影响的,但对于 h 最小的元素来说,k 就是它应该处在的位置(前面所有人都比他高,k = 2 时前面有 i = 0 和 i= 1 两个位置,此时他的位置 i = k = 2)。

再考虑 h 倒数第二小的元素,它的 i 和 k 不一定相等,因为如果刚才 h 最小的元素在它前面,它的 i 就是 k + 1。也就是说,按 h 升序的顺序,将每个元素放在剩余的空位置中的第 k 个位置即可成功排序。

  • 代码
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        int n = people.length;
        int[][] result = new int[n][2];
        for(int i = 0; i < n; i++){
            result[i][0] = 0;
            result[i][1] = -1;
        }

        Arrays.sort(people, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] - o2[0];
            }
        });

        int p = 0;
        while(p < n){
            //System.out.println(people[p][0] + "," + people[p][1]);
            int rank = people[p][1];
            for(int k = 0; k < result.length; k++){
                if(result[k][1] == -1){ // 空格子才能插入
                    if(rank == 0){
                        result[k][0] = people[p][0];
                        result[k][1] = people[p][1];
                        break;
                    } else {
                        rank--;
                    }
                } else {
                    if(result[k][0] == people[p][0]){
                        rank--;
                    }
                }
            }
            p++;
        }

        return result;
    }
}
  • 优化

result 数组可以不初始化成 -1,通过 null 也能判断。

1117|距离顺序排列矩阵单元格

给出 R 行 C 列的矩阵,其中的单元格的整数坐标为 (r, c),满足 0 <= r < R 且 0 <= c < C。

另外,我们在该矩阵中给出了一个坐标为 (r0, c0) 的单元格。

返回矩阵中的所有单元格的坐标,并按到 (r0, c0) 的距离从最小到最大的顺序排,其中,两单元格(r1, c1) 和 (r2, c2) 之间的距离是曼哈顿距离,|r1 - r2| + |c1 - c2|。(你可以按任何满足此条件的顺序返回答案。)

示例 1:
输入:R = 1, C = 2, r0 = 0, c0 = 0
输出:[[0,0],[0,1]]
解释:从 (r0, c0) 到其他单元格的距离为:[0,1]
示例 2:
输入:R = 2, C = 2, r0 = 0, c0 = 1
输出:[[0,1],[0,0],[1,1],[1,0]]
解释:从 (r0, c0) 到其他单元格的距离为:[0,1,1,2]
[[0,1],[1,1],[0,0],[1,0]] 也会被视作正确答案。
示例 3:
输入:R = 2, C = 3, r0 = 1, c0 = 2
输出:[[1,2],[0,2],[1,1],[0,1],[1,0],[0,0]]
解释:从 (r0, c0) 到其他单元格的距离为:[0,1,1,2,2,3]
其他满足题目要求的答案也会被视为正确,例如 [[1,2],[1,1],[0,2],[1,0],[0,1],[0,0]]。

提示:
1 <= R <= 100
1 <= C <= 100
0 <= r0 < R
0 <= c0 < C

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ma… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

暴力法天下第一。只要从给定的点开始按距离递增的方式遍历二维数组即可,最远距离作为循环的出口,中途过滤掉超出范围的坐标。

距离的计算方式是|r1 - r2| + |c1 - c2|,所以每个距离d 能拆分出 (dr=0d, dc=d0) 种的不同组合。

  • 代码
class Solution {
    public int[][] allCellsDistOrder(int R, int C, int r0, int c0) {
        int[][] result = new int[R*C][2];
        int max = calcMaxDistance(R,C,r0,c0);
        int cursor = 0;
        for(int i = 0; i <= max; i++){
            cursor = addDistance(R,C,r0,c0,result, i, cursor);
        }
        return result;
    }

    private int addDistance(int R, int C, int r0, int c0, int[][] collection, int distance, int cursor){
        if(distance == 0){
            collection[cursor][0] = r0;
            collection[cursor][1] = c0;
            return cursor+1;
        }
        int p = distance;
        int q = 0;
        int c = cursor;
        while(p >= 0 && q <= distance){
            if(p == 0){
            if(r0 + p < R && c0 - q >= 0){
                collection[c][0] = r0+p;
                collection[c][1] = c0-q;
                c++;
            }
            if(r0 + p < R && c0 + q < C){
                collection[c][0] = r0+p;
                collection[c][1] = c0+q;
                c++;
            }
                        p--;
            q++;
            continue;          
            }
            if(q == 0){
                if(r0 - p >=0 && c0 - q >= 0){
                collection[c][0] = r0-p;
                collection[c][1] = c0-q;
                c++;
            }
            if(r0 + p < R && c0 - q >= 0){
                collection[c][0] = r0+p;
                collection[c][1] = c0-q;
                c++;
            }
                        p--;
            q++;
                continue;
            }
            if(r0 - p >=0 && c0 - q >= 0){
                collection[c][0] = r0-p;
                collection[c][1] = c0-q;
                c++;
            }
            if(r0 + p < R && c0 - q >= 0){
                collection[c][0] = r0+p;
                collection[c][1] = c0-q;
                c++;
            }
            if(r0 + p < R && c0 + q < C){
                collection[c][0] = r0+p;
                collection[c][1] = c0+q;
                c++;
            }
            if(r0 - p >= 0 && c0 + q < C){
                collection[c][0] = r0-p;
                collection[c][1] = c0+q;
                c++;
            }

            p--;
            q++;
        }
        return c;
    }

    private int calcMaxDistance(int R, int C, int r0, int c0){
        return Math.max(R-r0-1,r0) + Math.max(C-c0-1, c0);
    }
}

1118|加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。

示例 1:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3

解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

示例 2:
输入:
gas = [2,3,4]
cost = [3,4,3]
输出: -1

解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ga… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

暴力法可解,模拟从每个位置出发的情况,计算到每个点的剩余油量,如果中途降为负数则认为不能行驶一周。如果有能回到出发点,则返回出发点的编号。

但暴力法存在大量冗余计算。如果从 a 可以出发,则说明 a 点出发的时候油量≥0,到 b 时发现油量为负数了,可以证明从 a+1 点出发也不能通过 b 点,a 到 b 之间的点都无法通过 b 点。

所以当遇到负值时,取下一个点作为起点即可,时间复杂度可以从 O(N²) 降低到 O(N)。

  • 代码
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {

        int i = 0;
        int n = gas.length;

        while(i < n){
            int sum = 0;
            int p = 0;
            while(p < n){
                int j = (i + p) % n;
                sum = sum + gas[j] - cost[j];
                if(sum < 0){
                    break;
                }
                p++;
            }
            if(p == n){
                return i;
            } else {
                i = i + p + 1;
            }
        }

        return -1;
    }
}
  • 技巧

int j = (i + p) % n; 用简短的代码实现循环访问数组。

1119|移动零

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

示例:

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

说明:

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

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/mo… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

由于只有0是特殊的,可以遍历数组,将每个不是0的数字按顺序重写到数组前面,记录已重写到的位置,后面补0即可。时间复杂度是 O(N)。

上面的做法可行,但成绩不太妙。整个数组都经历了一次重写,对于类似 [0,0,0,0,1] 这样的数据来说冗余操作太多了。使用双指针分别寻找0和下一个非零数,再交换二者的值,就能避免重写0的冗余操作了,从实测结果上看速度是有提升的。

  • 代码
class Solution {
    public void moveZeroes(int[] nums) {
        if(nums.length == 0){
            return;
        }
        int p = 0;
        int q = 0;

        while(true){
            while(nums[p] != 0){
                p++;
                if(p == nums.length){
                    return;
                }
            }

            q = p;
            while(nums[q] == 0){
                q++;
                if(q == nums.length){
                    return;
                }
            }
            nums[p] = nums[q];
            nums[q] = 0;
        }
    }
}
  • 技巧

大多数时候时间复杂度是同一级别的解法是不需要再区分优劣的,但不同的解法之间还是存在差异的。题目暗示了尽可能减少操作次数,应该多做考虑,找更优的解法。

1120|对链表进行插入排序

插入排序算法:

插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。 重复直到所有输入数据插入完为止。

示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/in… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

插入排序的思路是每次选一个新的元素,将它放到已排序线性表的正确位置,可以不使用额外空间,但这就需要移动元素,链表中如何移动元素就是这道题的一个难点了。

我们用一个节点将链表分为两部分,左侧维护已排序的元素,右侧是未经排序的元素,每次选右侧的一个节点插入到左侧。除了分界节点之外,完成移动元素还要记录前一个元素,还要处理插入到头结点之前的情况。

  • 代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode insertionSortList(ListNode head) {
        if(head == null){
            return head;
        }
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode lastSorted = head;
        ListNode curr = head;

        while(curr != null){
            if(lastSorted.val <= curr.val){
                lastSorted = curr;
            } else {
                ListNode temp = pre;
                while(temp.next.val < curr.val){
                    temp = temp.next;
                }
                lastSorted.next = curr.next;
                curr.next = temp.next;
                temp.next = curr;
            }
            curr = lastSorted.next;
        }

        return pre.next;
    }
}

1121|排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

进阶:
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/so… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

前一天刚做完插入排序,马上安排进阶练习针不戳。

排序是如同 abandon 一样让人难以忘记的算法基础知识了,直接看进阶吧。对数级别的时间复杂度,可能是快速排序、归并排序和堆排序之类的方案。加上链表的操作限制,归并排序是比较方便去实现的。

归并排序分两步,先分后合,分的过程要注意不重复不遗漏,合的过程要保证升序。拆分的时候需要获取链表的中间点,可以使用快慢指针实现,找中间点也是一道题来着,不深入解释了吧。

  • 代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        if(head == null){
            return head;
        }
        return sortList(head, null);
    }

    private ListNode sortList(ListNode head, ListNode tail){
        if(head == null){
            return head;
        }
        if (head.next == tail) {
            head.next = null;
            return head;
        }

        ListNode fast = head;
        ListNode slow = head;

        while(fast != tail){
            fast = fast.next;
            slow = slow.next;
            if(fast != tail){
                fast = fast.next;
            }
        }
        ListNode mid = slow;

        ListNode leftSorted = sortList(head, mid);
        ListNode rightSorted = sortList(mid, tail);

        return merge(leftSorted, rightSorted);
    }

    private ListNode merge(ListNode la, ListNode lb){
        ListNode pre = new ListNode(0);
        ListNode p = la;
        ListNode q = lb;
        ListNode u = pre;
        while(p != null || q != null){
            if(p == null){
                u.next = q;
                q = q.next;
            } else if(q == null){
                u.next = p;
                p = p.next;
            } else {
                if(p.val > q.val){
                    u.next = q;
                    q = q.next;
                } else {
                    u.next = p;
                    p = p.next;
                }
            }
            u = u.next;
        }

        return pre.next;
    }
}
  • 技巧

找链表的中点和合并有序链表都是曾经攻克过的难题(也可能不那么难),将复杂的问题拆分成简单问题的组合就是解题的过程了。

1122|有效的字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false

说明:
你可以假设字符串只包含小写字母。

进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/va… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 思路

字母异位词的含义是:

  1. 由完全相同种类的字母构成
  2. 每种字母的数量完全相同

用一个 HashMap 记录每个出现过的字母的数量就能用两次遍历得出结果了。时间复杂度是 O(N)。

  • 代码
class Solution {
    public boolean isAnagram(String s, String t) {
        if(s.length() != t.length()){
            return false;
        }
        HashMap<Character, Integer> map = new HashMap<>();
        for(int i = 0; i < s.length(); i++){
            if(map.get(s.charAt(i)) == null){
                map.put(s.charAt(i), 1);
            } else {
                Integer n = map.get(s.charAt(i));
                map.put(s.charAt(i), n+1);
            }
        }

        for(int j = 0; j < t.length(); j++){
            char c = t.charAt(j);
            if(map.get(c) == null){
                return false;
            }
            Integer m = map.get(c);
            if(m == 0){
                return false;
            }
            m = m - 1;
            map.put(c, m);
        }
        return true;
    }
}
  • 优化

HashMap 存在自动装箱/拆箱的问题,属于 Java 的一个弊端,这道题可以用一个 size 为 26 的数组替代 HashMap。

  • 另解

分别对 s 和 t 排序,再对比二者是否相等。