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… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
- 思路
字母异位词的含义是:
- 由完全相同种类的字母构成
- 每种字母的数量完全相同
用一个 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 排序,再对比二者是否相等。