Leetcode(剑指offer)
- 前 n 个数字二进制中 1 的个数
Brian Kernighan 算法: x = x & (x - 1);
---- 目的:可以将x的最后一个1转变成0,所以转变成0的次数,就是1的个数。每个数字的时间复杂度不超过O(logN)。
动态规划算法:bit[i] = bit[i - highBit] + 1;
---- 目的:highBit是从小到大,离得最近的一个2的整数次幂。相当于一个值减去最高位的一个1,bit[i - highBit] + 1,把1再加回来。
动态规划算法:bit[i] = bit[i >> 1] + (i & 1);
---- 目的:将最低一位移除,同时为了判断最低一位的数是0,还是1,使用i & 1来判断。
- 整数除法
快速乘算法:如果返回为false,则不保存,将右边减1,若返回true,将值保存,并将左边加1.
public boolean quickAdd(int x, int y, int z){
int result = 0, add = y;
while (z != 0) {
if ((z & 1) != 0) { // 如果z是奇数,将结果保存,因为每次z要除以2,必须保证是区间是在2的整数幂之间
if (result < x - add) { // 奇数个超过被除数,返回false。没超过这个被除数,则保存。
return false;
}
result += add;
}
if (z != 1) { // 找出在哪两个2的整数幂之间,N,N+1,然后再从他们之间继续寻找。
if (add < x - add) {
return false;
}
add += add;
}
z >>= 1; // 它记录的是add的偶次幂
}
return true;
}
public int quickAdd(int x, int y) {
boolean rev = false;
if (x > 0) {
x = -x;
rev = !rev;
}
if (y > 0) {
y = -y;
rev = !rev;
}
List<Integer> store = new ArrayList<>();
store.add(y);
int index = 0;
while (store.get(index) >= x - store.get(index)) {
store.add(store.get(index) + store.get(index));
index++;
}
int ans = 0;
for (int i = store.size() - 1; i >= 0; i--) {
if (store.get(i) >= x) {
ans += 1 << i;
x -= store.get(i);
}
}
return rev ? -ans : ans;
}
3.二进制加法
因为考虑到字符串的长度问题,不能单纯的用Long.parseLong(a,2)。所以使用字符串拼接,然后再反转字符串。
使用carry来计算1的个数,carry/=2取整作为进位数,carry%=2作为保存的数。
关键算法:
for (int i = 0; i < n; ++i) {
carry += i < a.length() ? (a.charAt(a.length() - 1 - i) - '0') : 0;
carry += i < b.length() ? (b.charAt(b.length() - 1 - i) - '0') : 0;
ans.append((char) (carry % 2 + '0'));
carry /= 2;
}
python中,可以使用:
class Solution:
def addBinary(self, a, b) -> str:
x, y = int(a, 2), int(b, 2)
while y:
answer = x ^ y # 异或想加,将x和y不产生进位的数相加。
carry = (x & y) << 1 # 同时为1时,向前移动1位置,得到的结果时进位。
x, y = answer, carry
return bin(x)[2:] # 取前两位
4.只出现一次的数字
关键算法:(x >> i) & 1,每次取最后一位,验证是0还是1。
算法思想:每个数字至少出现3次,只有一个出现1次,所以对于位运算来说,每一位除了0就是1,并且都是三的倍数。 total % 3余数为1,将1左移相应的i位,并将结果进行按位累加(取或|),得到的值就是出现一次的值。
关键算法:
for (int i = 0; i < 32; ++i) {
int total = 0;
for (int num: nums) {
total += ((num >> i) & 1);
}
if (total % 3 != 0) {
ans |= (1 << i);
}
}
5.单词长度的最大乘积
关键算法:masks[i] |= 1 << (word.charAt(j) - 'a'); 和 masks[i] & masks[j]) == 0;
算法思想:使用26位来记录26个字母,如果有对应位置写1,从右到左位a-z。如果他们的与结果为0,则表示不同。
求不同的最大成绩:maxProd = Math.max(maxProd, words[i].length() * words[j].length()),不断更新最大值。
6.排序数组中两个数字之和
关键算法:int low = 0, high = numbers.length - 1;
算法思想:使用双指针寻找,双指针之和大于目标值,则将右指针左移;双指针之和小于目标值,则将左指针右移;
7数组中和为 0 的三个数
关键算法:sort()、 if (first > 0 && nums[first] == nums[first - 1]) {continue;}、int third = n - 1;、int target = -nums[first];
算法思想:这里是找到三个数来构成一个等式。通过排序可以优化代码,然后过滤掉相同的数字,固定第一个数,第二个数和第三个数可以用双指针来指定,第二个数也需要遍历,所以当seconde+third>-target,则将third左移,相等则保存,小于则第二个遍历继续。
关键算法:
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<List<Integer>>();
for (int first = 0; first < n; ++first) {
// 需要和上一次枚举的数不相同
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; ++second) {
// 需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
ans.add(list);
}
}
}
return ans;
8.和大于等于 target 的最短子数组
关键算法: ans = Math.min(ans, end - start + 1)、sum -= nums[start]、 start++
算法思想:指定一个滑动的窗口,当满足条件时候,保存长度,然后将窗口从左边缩小,同时还会更新长度,直到不满足条件。然后重新开始从右边将窗口扩大。
关键算法:
int ans = Integer.MAX_VALUE;
int start = 0, end = 0;
int sum = 0;
while (end < n) {
sum += nums[end];
while (sum >= s) {
ans = Math.min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
9.乘积小于 K 的子数组
关键算法: ret += j - i + 1;
算法思想:滑动窗口的思想,窗口内都是满足条件的数,使用ret+=j-i+1的方法,可以将最新的组合个数与之前的个数累计(增加一个新元素,会增加j-i+1个子空间)。
关键算法: for (int j = 0; j < n; j++) {
prod *= nums[j];
while (i <= j && prod >= k) {
prod /= nums[i];
i++;
}
ret += j - i + 1;
}
10.和为 k 的子数组
关键算法:count += mp.get(pre - k);、mp.put(pre, mp.getOrDefault(pre, 0) + 1);
算法思想:因为有负数,所以滑动窗口并不能满足往右即为增加,本题使用将前缀和保存的方式,然后通过两个前缀的差值与k相同,则将前缀和的数量累加。
关键算法:
mp.put(0,1) // 因为后面是先统计个数,所以默认0出现的次数为1,如果真的出现,也是先为1,只有再次为0的时候才是在2的基础上加1,因为0+任何数都不改变值,都会在原先的基础上加1,所以默认为1。
for (int i = 0; i < nums.length; i++) {
pre += nums[i]; // 求前缀和
if (mp.containsKey(pre - k)) { // pre和另一个前缀和的差值等于k,前缀和出现,就要保存前缀和出现的个数。
count += mp.get(pre - k);
}
mp.put(pre, mp.getOrDefault(pre, 0) + 1); // 如果前缀和是第一次出现,则默认值为0+1,如果是再次出现则在原来的基础上加1。因为这里是将累加后的pre加到map中,所以不会出现和为0却没有出现的情况(int sum=0,实际并没有累加)。
}
11.0 和 1 个数相同的子数组
关键算法:
算法思想:遇到0则减1,遇到1则加1,查看map中是否已经存在,如果存在这个值,说明符合相同的个数,则用当前下表减去它的下标,只有不存在这个个数的时候才会新加入map,所以统计的是最先遇到个数,这样随着遍历的后移可以保证取得是最长的字串。
关键算法:
map.put(counter, -1);
int n = nums.length;
for (int i = 0; i < n; i++) {
int num = nums[i];
if (num == 1) {
counter++;
} else {
counter--;
}
if (map.containsKey(counter)) {
int prevIndex = map.get(counter);
maxLength = Math.max(maxLength, i - prevIndex);
} else {
map.put(counter, i);
}
}
12.左右两边子数组的和相等
关键算法:2 * sum + nums[i] == total;
算法思想:一个数的左右两边相等即为2*sum,加上当前的值,等于total。
关键算法:
for (int i = 0; i < nums.length; ++i) {
if (2 * sum + nums[i] == total) {
return i;
}
sum += nums[i];
}
13.二维子矩阵的和
关键算法:sums[i][j + 1] = sums[i][j] + matrix[i][j];
算法思想:sum[i][j]表示的是当前i行和当前j列之前的所有值,加上移动的值,等于下一个值,相当于二维平面的动态规划。但是这个方法是一维前缀和,使用一维来一步步变到二维上。
关键算法:
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i][j + 1] = sums[i][j] + matrix[i][j];
}
}
优化:
关键算法:sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
算法思想:他利用的是对角线的二维前缀和,相当于面积的计算,当前对角线的面积等于左边对角线和上边对角线之和,再减去重复部分,加上对角线的数据。
关键算法:
sums = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
}
}
14.字符串中的变位词
关键算法:++cnt1[s1.charAt(i) - 'a'];++cnt2[s2.charAt(i) - 'a'];、Arrays.equals(cnt1, cnt2)
算法思想:因为是变位词,所以只有在窗口内具有相同个数相同字符的字串就可以,并且窗口大小不变。使用cnt1和cnt2分别统计两个字符串是不是最前面的就相等,如果不是,就可以采用遍历的方式比较后面的。使用++cnt2和--cnt2就可以实现窗口的走动带来的字符变化。每次走动之后,比较一下是否相等。
关键算法:
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
for (int i = n; i < m; ++i) {
++cnt2[s2.charAt(i) - 'a'];
--cnt2[s2.charAt(i - n) - 'a'];
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
}
方法二:
关键算法:
++cnt[x];
while (cnt[x] > 0) {
--cnt[s2.charAt(left) - 'a'];
++left;
}
算法思想:在一个cnt中取统计,要找的源字符串当前位-1即可。在对目标字符串进行遍历时候,如果加入的数字大于0,说明超了,这个字符加入后,发生了错误,所以,我们移动左边的字符,来改变窗口的开始位置,直到长度正合适时,放回true。
关键算法:
for (int right = 0; right < m; ++right) {
int x = s2.charAt(right) - 'a';
++cnt[x];
while (cnt[x] > 0) {
--cnt[s2.charAt(left) - 'a'];
++left;
}
if (right - left + 1 == n) {
return true;
}
15.字符串中的所有变位词
关键算法:ans.add(i + 1);
算法思想:同上,只是保存了i。
关键算法:
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s.charAt(i) - 'a'];
++sCount[s.charAt(i + pLen) - 'a'];
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
16.不含重复字符的最长子字符串
关键算法:occ.remove(s.charAt(i - 1));、occ.add(s.charAt(rk + 1));、ans = Math.max(ans, rk - i + 1);
算法思想:因为查找连续的最长字串,使用滑动窗口,每次将要加入的字符串如果存在,则将左边界右移,同时set中删除该元素。直到将要加入的元素不在set中为止,然后开始下一轮的不重复记录,不重复右边界右移,每次记录完,记录最大值。
关键算法:
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.remove(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
17.含有所有字符的最短字符串
关键算法:check() && l <= r
算法思想:滑动窗口,首先最小窗口,右指针扩大的时候,是否包含了全部的字符,如果包含全部字符,记录窗口大小,然后开始缩小窗口,直到不满足条件时,开始右边扩大窗口,直到结束时,最终的记录就是最小的窗口。
关键算法:
while (r < sLen) { // 右移边界
++r; // 先移位
if (r < sLen && ori.containsKey(s.charAt(r))) { // 只查找源字符串存在的字符,目标字符串存在,则加1
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
while (check() && l <= r) { // check()是自定义的函数,返回boolean类型,通过遍历源目标的字符和个数,检查目标字符串内是否相同。
if (r - l + 1 < len) { // 用于保存最小长度,记录此时的左位置和右位置
len = r - l + 1;
ansL = l;
ansR = l + len;
}
if (ori.containsKey(s.charAt(l))) { // 记录好之后,则开始移动窗口左边的指针。只记录对结果有影响的字符,也就是源目标在窗口左边包含的字符。
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
18.有效的回文
关键算法:sgood.toString().equals(sgood_rev.toString());
算法思想:字符串翻转,也相同。
关键算法:
while (left < right) {
if (Character.toLowerCase(sgood.charAt(left)) != Character.toLowerCase(sgood.charAt(right))) {
return false;
}
++left;
--right;
}
方法二:
关键算法:Character.toLowerCase(sgood.charAt(left)) != Character.toLowerCase(sgood.charAt(right))
算法思想:双指针。
关键算法:
while (left < right) {
if (Character.toLowerCase(sgood.charAt(left)) != Character.toLowerCase(sgood.charAt(right))) {
return false;
}
++left;
--right;
}
19.最多删除一个字符得到回文
关键算法:validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high);
算法思想:双指针。左右指针相同的时候,照常判断,左右指针不同的时候,判断左+1或者右-1是否是回文。
关键算法:
while (low < high) {
char c1 = s.charAt(low), c2 = s.charAt(high);
if (c1 == c2) {
++low;
--high;
} else {
return validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high);// validPalindrome方法是判断是否是回文。
}
}
20.回文子字符串的个数
关键算法:int l = i / 2, r = i / 2 + i % 2;
算法思想:中心拓展。通过观察和计算,奇数个字符串和偶数个字符串可以通过数据的处理来判断,当以[l,r]为回文中心的字符时,每次只要保证左右字符相同即可。
关键算法:
for (int i = 0; i < 2 * n - 1; ++i) {
int l = i / 2, r = i / 2 + i % 2; // 处理字符串
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) { // 确保相同
--l;
++r;
++ans;
}
}
21.删除链表的倒数第 n 个结点
关键算法:for (int i = 0; i < n; ++i) {first = first.next;}
算法思想:使用双指针,先将第一个指针移动n位,然后两个指针一起移动,当第一个指针指到最后一位时,第二个指针落在倒数第n个指针前面一个,只需要删除第二个指针后面一个即可。
关键算法:
for (int i = 0; i < n; ++i) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
22.链表中环的入口节点
关键算法:
算法思想:两个指针相遇的时刻,慢指针一定没跑完一圈(慢指针跑完一圈,快指针都跑两圈了)。快指针跑的路程=a+n(b+c)+b,是慢指针=(a+b)的两倍。其中,a为环外距离,b为相遇点到入口的距离,c为环剩下的距离。
解等式a+n(b+c)+b=2(a+b) => a=c+(n-1)(b+c),b+c是一圈。
也就是说,如果此时,甲从起始点出发,走a距离,乙从相遇点出发,走了n-1圈和c距离,此时乙和甲会在入口点相遇。
关键算法:
```java
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
- 两个链表的第一个重合节点
关键算法:pA = pA == null ? headB : pA.next;、pB = pB == null ? headA : pB.next;
算法思想:双指针,一个指针指向a,一个指针指向b,他俩同时出发,哪一个结束了,就换到对方阵营去走,直到相遇。因为如果他们有交集,路程相同的话,到最后一定会相遇。
关键算法:
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
24.反转链表
关键算法:ListNode prev = null;、ListNode curr = head、curr.next = prev; // 连接前节点
算法思想:迭代,一种前插的思路,
关键算法:
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next; // 指向下一个要插入的节点
curr.next = prev; // 连接前节点
prev = curr; // 变换前节点
curr = next; // 开始下一个节点
}
25.链表中的两数相加
关键算法:!stack1.isEmpty() || !stack2.isEmpty() || carry != 0)、
算法思想:使用栈的方式,两个链表压入栈中,这样出栈的时候就是最后一个相加了。
关键算法:
while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) { // 不为空,或有进位
int a = stack1.isEmpty() ? 0 : stack1.pop();
int b = stack2.isEmpty() ? 0 : stack2.pop();
int cur = a + b + carry;
carry = cur / 10; // 进位
cur %= 10; // 保存的值
ListNode curnode = new ListNode(cur);
curnode.next = ans; // 使用头插入
ans = curnode;
}
26.重排链表
关键算法:
算法思想:寻找链表中点 + 链表逆序 + 合并链表
关键算法:
ListNode mid = middleNode(head);
ListNode l1 = head;
ListNode l2 = mid.next;
mid.next = null;
l2 = reverseList(l2);
mergeList(l1, l2);
27.回文链表
关键算法:
算法思想:递归
关键算法:
class Solution {
private ListNode frontPointer; // 将frontPointer提出全局
private boolean recursivelyCheck(ListNode currentNode) {
if (currentNode != null) {
if (!recursivelyCheck(currentNode.next)) { // 递归,如果返回的是false,将会一直返回false,直到停止。
return false;
}
if (currentNode.val != frontPointer.val) { // 此轮询不相等返回false
return false;
}
frontPointer = frontPointer.next;// 后面的节点由递归变化,前面的节点手动变化
}
return true;
}
public boolean isPalindrome(ListNode head) {
frontPointer = head;
return recursivelyCheck(head);
}
}
关键算法:
算法思想:快慢指针。先找到中间指针(奇数),中间指针的第一个(偶数)。然后将后面的反转,并遍历直到结束。如果找不出异常,则返回true。
关键算法:
// 找到前半部分链表的尾节点并反转后半部分链表
ListNode firstHalfEnd = endOfFirstHalf(head);// 后半段的第一个值
ListNode secondHalfStart = reverseList(firstHalfEnd.next);// 反转,头插法
// 判断是否回文
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean result = true;
while (result && p2 != null) {
if (p1.val != p2.val) {
result = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 还原链表并返回结果
firstHalfEnd.next = reverseList(secondHalfStart);
return result;
28.展平多级双向链表
关键算法:
算法思想:深度优先搜索。有一个父节点的last指针指向最后一个,有一个子节点的指针指向子节点的最后一个,如果子节点不空,则遍历子节点。
关键算法:
class Solution {
public Node flatten(Node head) {
dfs(head); // 进入递归
return head;
}
public Node dfs(Node node) {
Node cur = node; // 记录当前节点
Node last = null;// 记录父节点的last
while (cur != null) { // 当前节点不为空
Node next = cur.next; // 保存下一节点,处理当前节点
if (cur.child != null) { // 如果有子节点,那么首先处理子节点
Node childLast = dfs(cur.child); // 将子节点作为新一轮的父节点,返回的是子节点的最后一个节点
next = cur.next; // 父节点的next
// 将 node 与 child 相连
cur.next = cur.child; // 将两个链表相连
cur.child.prev = cur;
// 如果 next 不为空,就将 last 与 next 相连
if (next != null) { // 父节点后面还有,则将子节点的尾节点连接后面的节点
childLast.next = next;
next.prev = childLast;
}
// 将 child 置为空
cur.child = null;
last = childLast; // 连接后,从子节点的最后一个节点开始
} else {
last = cur;
}
cur = next;
}
return last;
}
}
29.排序的循环链表
关键算法:
算法思想:一次遍历。找到要插入的位置,即大于cur节点,小于next节点。但是当到尾节点的时候,需要单独的考虑条件,因为此时的cur节点是大于next(头)节点的。
关键算法:
class Solution {
public Node insert(Node head, int insertVal) {
Node node = new Node(insertVal);
if (head == null) {
node.next = node;
return node;
}
if (head.next == head) {
head.next = node;
node.next = head;
return head;
}
Node curr = head, next = head.next;
while (next != head) {
if (insertVal >= curr.val && insertVal <= next.val) {
break;
}
if (curr.val > next.val) { // 尾巴单独算
if (insertVal > curr.val || insertVal < next.val) {
break;
}
}
curr = curr.next; // 移动
next = next.next;
}
curr.next = node; // 在curr和next之间插入一个值
node.next = next;
return head;
}
}
30.插入、删除和随机访问都是 O(1) 的容器
关键算法:
算法思想:变长数组 + 哈希表。变长数组可以在 O(1)O(1)O(1) 的时间内完成随机访问元素操作,哈希表可以在 O(1)的时间内完成插入和删除操作。
插入逻辑:如果哈希表中没有,则满足插入条件,将插入到变长数组后。
删除逻辑:如果哈希表中有,则满足删除条件,将变长数组中组后一个数,放到删除的索引下,并将长度减一。
查找逻辑:直接随机选取一个查询。
关键算法:
public RandomizedSet() {
nums = new ArrayList<Integer>(); // 可变长数组,用于查询
indices = new HashMap<Integer, Integer>(); // 哈希表(值,索引),
random = new Random();
}
31.最近最少使用缓存
关键算法:
算法思想:哈希表 + 双向链表。
get方法:首先查看缓存中是否存在,不存在返回-1,存在,则将节点移到头部,然后将返回值。
put方法:首先查看缓存中是否存在,如果存在,则将值修改,不存在,则插入到头部,插入后判断长度是否正常,不正常的话,则删除尾部节点。
关键算法:
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
32.有效的变位词
关键算法:Arrays.sort(str1);、Arrays.sort(str2);
算法思想:排序/先比较,不相同则排序,然后再次比较。
关键算法:
Arrays.sort(str1);
Arrays.sort(str2);
return Arrays.equals(str1, str2);
方法二:
关键算法:
算法思想:哈希表,将一组字符放到数组中,并统计个数加1,在统计下一个字符时,减1,并判断每个字符的个数是否小于0,小于0则返回false。
关键算法:
for (int i = 0; i < t.length(); i++) {
table[t.charAt(i) - 'a']--;
if (table[t.charAt(i) - 'a'] < 0) {
return false;
}
}
33.变位词组
关键算法:Arrays.sort(array);map.put(key, list);
算法思想:排序。先将字符抓换成字符串,然后进行排序,排序后重新组成的字符串为key,然后查看map是否有保存过key,来进行添加还是新建。将原始字符串加入到value中。并覆盖保存。
关键算法:
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray(); // 准备将每个字符串转换成字符,排序
Arrays.sort(array);
String key = new String(array); // 将排序后的字符转换为字符串,作为key
List<String> list = map.getOrDefault(key, new ArrayList<String>()); // 去除key对应的list,没有的话就新建一个
list.add(str); // 将原始字符串添加进list
map.put(key, list); // 将key和value保存
}
return new ArrayList<List<String>>(map.values());
}
方法二:
关键算法:
for (int i = 0; i < length; i++) {
counts[str.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
sb.append((char) ('a' + i));
sb.append(counts[i]);
}
}
算法思想:计数。与方法一不同的是,寻找key的操作不是通过排序的方法,二是通过对26个字符进行计数。技术之后再将字符取出,然后将取出的字符进行拼接,形成key,value的取法和方法一一致。
关键算法:
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
int[] counts = new int[26];
int length = str.length();
for (int i = 0; i < length; i++) {// 去除字符,进行计数
counts[str.charAt(i) - 'a']++;
}
// 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
sb.append((char) ('a' + i));
sb.append(counts[i]);
}
}
String key = sb.toString();
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
34.外星语言是否排序
关键算法:
int[] index = new int[26];
for (int i = 0; i < order.length(); ++i) {
index[order.charAt(i) - 'a'] = i;
}
算法思想:字符数字化。我们需要比较字符串中每个字符再字典中的索引大小,我们将每个字符作为索引,将索引作为值,在比较的时候只需要提取出这个值对应的索引,进行比较就可以。在这里要考虑如果超过字符串之后的情况。
关键算法:
public boolean isAlienSorted(String[] words, String order) {
int[] index = new int[26];
for (int i = 0; i < order.length(); ++i) { // 处理字典,将索引作为值。
index[order.charAt(i) - 'a'] = i;
}
for (int i = 1; i < words.length; i++) {
boolean valid = false;
for (int j = 0; j < words[i - 1].length() && j < words[i].length(); j++) { // 从第一位开始比较,并且不能超过两个字符串的长度
int prev = index[words[i - 1].charAt(j) - 'a']; // 取出字典中的值进行比较
int curr = index[words[i].charAt(j) - 'a'];
if (prev < curr) { // 符合条件,修改状态为true,结束,并进行下两个字符串的比较。
valid = true;
break;
} else if (prev > curr) { // 不符合条件,直接返回false
return false;
}
}
if (!valid) { // 都比较完后,如果valid没有变化,即字符都相等,则比较长度
/* 比较两个字符串的长度 */
if (words[i - 1].length() > words[i].length()) { // 前面的长度大,不符合要求,返回false
return false;
}
}
}
return true;
}
35.最小时间差
关键算法:int n = timePoints.size();
if (n > 1440) {
return 0;
}
for (int i = 1; i < n; ++i) {
int minutes = getMinutes(timePoints.get(i));
ans = Math.min(ans, minutes - preMinutes); // 相邻时间的时间差
preMinutes = minutes;
}
算法思想:鸽巢原理。一共有24*60=1440个坑,如果多了,则说明有一样的,直接返回0。如果少了,则进行处理:先将数据排序,然后遍历,使用将时钟转换的方法,将时钟进行相减。因为时钟是个环,所以最后要考虑首位和尾位的插值,在首位的原有基础上加上1440,再和尾位相减。
关键算法:
public int findMinDifference(List<String> timePoints) {
int n = timePoints.size();
if (n > 1440) {
return 0;
}
Collections.sort(timePoints);
int ans = Integer.MAX_VALUE;
int t0Minutes = getMinutes(timePoints.get(0));
int preMinutes = t0Minutes;
for (int i = 1; i < n; ++i) {
int minutes = getMinutes(timePoints.get(i));
ans = Math.min(ans, minutes - preMinutes); // 相邻时间的时间差
preMinutes = minutes;
}
ans = Math.min(ans, t0Minutes + 1440 - preMinutes); // 首尾时间的时间差
return ans;
}
public int getMinutes(String t) {
return ((t.charAt(0) - '0') * 10 + (t.charAt(1) - '0')) * 60 + (t.charAt(3) - '0') * 10 + (t.charAt(4) - '0');
}
36.后缀表达式
关键算法:Deque<Integer> stack = new LinkedList<Integer>();stack.push(Integer.parseInt(token));
算法思想:栈。使用linkedList创建一个栈,遍历字符数组,遇到数字就加入到栈中,遇到字符就取出两个操作数,并进行运算,将结果重新入栈。
关键算法:
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<Integer>();
int n = tokens.length;
for (int i = 0; i < n; i++) {
String token = tokens[i];
if (isNumber(token)) {
stack.push(Integer.parseInt(token));
} else {
int num2 = stack.pop();
int num1 = stack.pop();
switch (token) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
default:
}
}
}
return stack.pop();
}
public boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}
37.小行星碰撞
关键算法:
算法思想:栈。一直遍历,加入栈中,当栈顶为正,遇到负的行星,则进行判断,大于栈顶的话,栈顶删除,并将新的入栈,等于的话,将栈顶删除,不将新的入栈,小于的话,不删除也不入栈。是否入栈用alive表示。
关键算法:
public int[] asteroidCollision(int[] asteroids) {
Deque<Integer> stack = new ArrayDeque<Integer>();
for (int aster : asteroids) {
boolean alive = true;
while (alive && aster < 0 && !stack.isEmpty() && stack.peek() > 0) {
alive = stack.peek() < -aster; // 只有当栈顶的值小于后面行星时,这个时候要删除栈顶,并且将新的行星放进去。
if (stack.peek() <= -aster) { // 栈顶的行星小于等于刚遍历的行星时,删除栈顶,准备加入新的行星。
stack.pop();
}
}
if (alive) { // 加入新的行星,当栈顶的值大于后面行星时,只需要删除,不需要更新呢。如果alive不变的话,将一直加入
stack.push(aster);
}
}
int size = stack.size();
int[] ans = new int[size];
for (int i = size - 1; i >= 0; i--) {
ans[i] = stack.pop();
}
return ans;
}
38.每日温度
关键算法:
算法思想:暴力。设置一个存储答案的数组,另一个记录next温度的下标(只保留最近的那个)。从后往前遍历(因为要找到后面第一个比他高的温度,所以从后面开始记录,从前面记录没有意义)。以上帝视角来告诉他,哪个离他最近的。首先将next都设为最大值。当温度出现的时候,进行更改。同时后面的都已经记录,next的变化也不会对后面造成影响。
关键算法:
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ans = new int[length];
int[] next = new int[101];
Arrays.fill(next, Integer.MAX_VALUE);
for (int i = length - 1; i >= 0; --i) {
int warmerIndex = Integer.MAX_VALUE;
for (int t = temperatures[i] + 1; t <= 100; ++t) {
if (next[t] < warmerIndex) {
warmerIndex = next[t];
}
}
if (warmerIndex < Integer.MAX_VALUE) {
ans[i] = warmerIndex - i;
}
next[temperatures[i]] = i;
}
return ans;
}
方法二:
关键算法:
算法思想:单调栈。什么时候写入的问题。只要遇到比他大的值,就具备了写入的条件。先移除,再写入结果。遍历数组,遇到比栈顶大的值,是出栈的机会。
关键算法:
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ans = new int[length];
Deque<Integer> stack = new LinkedList<Integer>();
for (int i = 0; i < length; i++) {
int temperature = temperatures[i];
while (!stack.isEmpty() && temperature > temperatures[stack.peek()]) { // 为空时,说明没有数据比较了,需要入栈。当遇到温度大的,将会进行出栈。
int prevIndex = stack.pop();
ans[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return ans;
}
39.直方图最大矩形面积
关键算法:
算法思想:分别以每个数值为高,找它的最长长度(宽)。如:[6,7,5,2,4,5,9,3],以6为高的宽应该从哪里到哪里。所以分为左右来确定这个宽。左边:只要高度大于等于当前的高度就可以。右边:同样大于等于当前高度就可以。怎么找?利用栈。
左边:左边是来找该高度的最小索引应该从哪开始。在栈里只能大的值压小的值,如果能压得住,说明该高度的最左索引只能是它压得数字的索引(如:7压6,所以以7为高度,只能从6的索引开始,但不包括6的索引)。如果压不住,就一直弹出,找到能压的位置。(如:5压不住栈里的7,将7弹出,也压不住6,将6弹出,所以以5为高度,只能从索引-1开始,不包括-1)。
右边:右边是来找该高度以哪结束。同左边,能压得住说明以它能压住的那个为结束。如(9能压住3,所以9在3的索引结束,5压不住9,所以9出栈,能压住3,从3结束)。
优化:
右边也可以以出栈时候的位置为结束。如:在找左边的时候,是5这个位置让7出去的,也就是说当开始处理5的时候,这个索引也是7的结束索引。所以可以在对左边处理时,在出栈前加上让栈顶弹出的结束索引,如7的结束索引5。
关键算法:
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(right, n);
Deque<Integer> mono_stack = new ArrayDeque<Integer>();
for (int i = 0; i < n; ++i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) { // 栈顶太大,就要弹出
right[mono_stack.peek()] = i; // 让他弹出的这个索引,就是它的结束索引
mono_stack.pop(); // 出栈
}
left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek()); // 开始索引,当为空时,为-1,否则,第一个它能压住的索引,就是它的开始索引。
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]); // 宽度减一乘以高度
}
return ans;
}
}
40.矩阵中最大的矩形
关键算法:
算法思想:结合39,统计结束位置和开始位置分别为down和up,他们的插值为高,当前值为宽。不过要先将数据进行类似39的处理,为1时都要累加,为0时则不变,同时的列全为0。
关键算法:
public int maximalRectangle(String[] matrix) {
int m = matrix.length;
if (m == 0) {
return 0;
}
int n = matrix[0].length();
int[][] left = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i].charAt(j) == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
int[] up = new int[m];
int[] down = new int[m];
Deque<Integer> stack = new ArrayDeque<Integer>();
for (int i = 0; i < m; i++) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
up[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
for (int i = m - 1; i >= 0; i--) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
down[i] = stack.isEmpty() ? m : stack.peek();
stack.push(i);
}
for (int i = 0; i < m; i++) {
int height = down[i] - up[i] - 1;
int area = height * left[i][j];
ret = Math.max(ret, area);
}
}
return ret;
41.滑动窗口的平均值
关键算法:
算法思想:使用队列模拟窗口。队列大小即窗口大小。
关键算法:
class MovingAverage {
Queue<Integer> queue;
int size;
double sum;
public MovingAverage(int size) {
queue = new ArrayDeque<Integer>();
this.size = size;
sum = 0;
}
public double next(int val) {
if (queue.size() == size) {
sum -= queue.poll(); // 删除队头
}
queue.offer(val); // 队列中加入
sum += val;
return sum / queue.size();
}
}
42.最近请求次数
关键算法:
算法思想:使用队列来判断是否将队列的队头删除。不达到条件的t将移出队列。
关键算法:
class RecentCounter {
Queue<Integer> queue;
public RecentCounter() {
queue = new ArrayDeque<Integer>();
}
public int ping(int t) {
queue.offer(t);
while (queue.peek() < t - 3000) {
queue.poll();
}
return queue.size();
}
}
43.往完全二叉树添加节点
关键算法:
算法思想:使用两个队列,一个队列存放原始的二叉树。一个队列用来确定当前新添的节点的父节点。
关键算法:
class CBTInserter {
Queue<TreeNode> candidate; // 应该插入的父节点队列
TreeNode root;
public CBTInserter(TreeNode root) {
this.candidate = new ArrayDeque<TreeNode>();
this.root = root;
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) { // 根节点可能不为空,所以使用队列来将根节点及这个根节点的其他节点,都遍历。
TreeNode node = queue.poll();
if (node.left != null) { // 左节点不为空,将左节点入队
queue.offer(node.left);
}
if (node.right != null) { // 右节点不为空,则将有节点入队
queue.offer(node.right);
}
if (!(node.left != null && node.right != null)) { // 如果该左节点和右节点没有满,则将这个节点加入到代插入新节点的父节点,即下一个节点的父节点就是队列的头
candidate.offer(node);
}
}
}
public int insert(int val) {
TreeNode child = new TreeNode(val);
TreeNode node = candidate.peek(); // 提取出父节点
int ret = node.val;
if (node.left == null) { // 父节点的左节点为空则插在左边。
node.left = child;
} else {
node.right = child; // 否则就插在右边,插了右边,需要将父节点队列的下一个节点做准备
candidate.poll();
}
candidate.offer(child);
return ret;
}
public TreeNode get_root() {
return root;
}
}
44.二叉树每层的最大值
关键算法:
算法思想:前序遍历。使用一个数组,只保存每一层的最大值。
关键算法:
class Solution {
public List<Integer> largestValues(TreeNode root) {
if (root == null) { // 根节点为空的情况
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<Integer>();
dfs(res, root, 0); // 深度遍历
return res;
}
public void dfs(List<Integer> res, TreeNode root, int curHeight) {
if (curHeight == res.size()) { // 判断是否是新的一层,层数是从0开始的,如果当前行还没有加入最大值,将加入最大值。
res.add(root.val);
} else { // 如果当前层已经有最大值了,需要进行比较,取出当前层的那个值和该值进行比较,并将最大值放到对应索引上。
res.set(curHeight, Math.max(res.get(curHeight), root.val));
}
if (root.left != null) { // 深度遍历,同时把层数也传入(前序遍历)
dfs(res, root.left, curHeight + 1);
}
if (root.right != null) { // 深度遍历,同时把层数传入
dfs(res, root.right, curHeight + 1);
}
}
}
方法二:
关键算法:
算法思想:层次遍历。使用一个数组,只保存每一层的最大值。
关键算法:
class Solution {
public List<Integer> largestValues(TreeNode root) {
if (root == null) {
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<Integer>();
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) { // 没有值,则遍历结束
int len = queue.size(); // 使用长度控制当前层的个数
int maxVal = Integer.MIN_VALUE;
while (len > 0) { // 队列中有数,需要将队列中的数一个个取出来,比较
len--;
TreeNode t = queue.poll();
maxVal = Math.max(maxVal, t.val); // 只要最大值
if (t.left != null) { // 将左子树放入队列
queue.offer(t.left);
}
if (t.right != null) { // 将右子树放入队列
queue.offer(t.right);
}
}
res.add(maxVal); // 结束后,将本行的最大值保存。
}
return res;
}
}
45.二叉树最底层最左边的值
关键算法:
算法思想:后序遍历。遍历并且加上高度,选择最高的那个高度,保存它的值。
关键算法:
class Solution {
int curVal = 0;
int curHeight = 0;
public int findBottomLeftValue(TreeNode root) {
int curHeight = 0;
dfs(root, 0);
return curVal;
}
public void dfs(TreeNode root, int height) {
if (root == null) {
return;
}
height++;
dfs(root.left, height);
dfs(root.right, height);
if (height > curHeight) {
curHeight = height;
curVal = root.val;
}
}
}
方法二:
关键算法:
算法思想:层次遍历。从右往左入队,最后一个就是最左边的值。
关键算法:
class Solution {
public int findBottomLeftValue(TreeNode root) {
int ret = 0;
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode p = queue.poll();
if (p.right != null) {
queue.offer(p.right);
}
if (p.left != null) {
queue.offer(p.left);
}
ret = p.val;
}
return ret;
}
}
46.二叉树的右侧视图
关键算法:
算法思想:利用两个栈,分别保存层数和当前的左子树和右子树,并不断更新当前行的值,最后一个更新的就是当前行的最后一个可见的值。
关键算法:
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();
int max_depth = -1;
Queue<TreeNode> nodeQueue = new ArrayDeque<TreeNode>();
Queue<Integer> depthQueue = new ArrayDeque<Integer>();
nodeQueue.add(root);
depthQueue.add(0);
while (!nodeQueue.isEmpty()) {
TreeNode node = nodeQueue.remove();
int depth = depthQueue.remove();
if (node != null) {
// 维护二叉树的最大深度
max_depth = Math.max(max_depth, depth);
// 由于每一层最后一个访问到的节点才是我们要的答案,因此不断更新对应深度的信息即可
rightmostValueAtDepth.put(depth, node.val);
nodeQueue.add(node.left);
nodeQueue.add(node.right);
depthQueue.add(depth + 1);
depthQueue.add(depth + 1);
}
}
List<Integer> rightView = new ArrayList<Integer>();
for (int depth = 0; depth <= max_depth; depth++) {
rightView.add(rightmostValueAtDepth.get(depth));
}
return rightView;
}
}
47.二叉树剪枝
关键算法:
算法思想:后序遍历。当根的左右字数都为空,并且自己的数字还是0,那么就没有要的必要了。
关键算法:
public TreeNode pruneTree(TreeNode root) {
if (root == null) {
return null;
}
root.left = pruneTree(root.left);
root.right = pruneTree(root.right);
if (root.left == null && root.right == null && root.val == 0) {
return null;
}
return root;
}
48.序列化与反序列化二叉树
关键算法:
算法思想:前序遍历。根据前序遍历,将树转换成字符串。然后使用字符串拆分将拆分的字符串组成列表,通过对列表的第一个元素的删除来模拟树的遍历,再次将树生成。
关键算法:
public class Codec {
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode deserialize(String data) {
String[] dataArray = data.split(",");
List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
return rdeserialize(dataList);
}
public String rserialize(TreeNode root, String str) { // 根为空则加入none,非空则进入前序遍历,并且保存每次遍历的字符串。
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public TreeNode rdeserialize(List<String> dataList) { // 拆分后的字符串,依次取出第一个。
if (dataList.get(0).equals("None")) {
dataList.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0))); // 取出第一个,生成节点
dataList.remove(0); // 删除第一个
root.left = rdeserialize(dataList); // 同样前序递归
root.right = rdeserialize(dataList);
return root; // 返回根节点
}
}
49.从根节点到叶节点的路径数字之和
关键算法:
算法思想:前序遍历。如果根不为空,则进行前序遍历,对根进行处理,在满足条件的时候返回,或者返回左右的和。
关键算法:
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}
public int dfs(TreeNode root, int prevSum) {
if (root == null) {
return 0;
}
int sum = prevSum * 10 + root.val;
if (root.left == null && root.right == null) {
return sum;
} else {
return dfs(root.left, sum) + dfs(root.right, sum);
}
}
}
方法二:
关键算法:
算法思想:层次遍历。增加俩个队列,一个用来进行树的出入栈,一个用来计算值的变化。
关键算法:
class Solution {
public int sumNumbers(TreeNode root) {
if (root == null) {
return 0;
}
int sum = 0;
Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();
Queue<Integer> numQueue = new LinkedList<Integer>();
nodeQueue.offer(root);
numQueue.offer(root.val);
while (!nodeQueue.isEmpty()) {
TreeNode node = nodeQueue.poll();
int num = numQueue.poll();
TreeNode left = node.left, right = node.right;
if (left == null && right == null) {
sum += num;
} else {
if (left != null) {
nodeQueue.offer(left);
numQueue.offer(num * 10 + left.val);
}
if (right != null) {
nodeQueue.offer(right);
numQueue.offer(num * 10 + right.val);
}
}
}
return sum;
}
}
50.向下的路径节点之和
关键算法:
算法思想:前序遍历+前缀和。以每个节点作为根节点,统计每个节点有多少个满足条件的路径。遍历时采用前序遍历,前缀和采用前序遍历。
关键算法:
class Solution {
public int res;
public int pathSum(TreeNode root, int targetSum) {
// 每个节点的左右子树是否满足条件
if (root == null) {
return res;
}
countNum(root,targetSum,0);
pathSum(root.left,targetSum);
pathSum(root.right,targetSum);
return res;
}
public void countNum(TreeNode root, int targetSum, long sum) {
if (root == null) {
return ;
}
sum += root.val;
if (sum == targetSum) {
res++;
}
countNum(root.left,targetSum,sum);
countNum(root.right,targetSum,sum);
}
}
51.节点之和最大的路径
关键算法:
算法思想:后序遍历。将根节点与左右子树相加,取最大的和。返回值为左右子树中大的数和父节点的和。
关键算法:
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}
52.展平二叉搜索树
关键算法:
算法思想:中序遍历。将中序遍历的结果保存在list中,然后遍历list,创建节点。可以先创建一个假的头节点。
关键算法:
class Solution {
public TreeNode increasingBST(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
inorder(root, res);
TreeNode dummyNode = new TreeNode(-1);
TreeNode currNode = dummyNode;
for (int value : res) {
currNode.right = new TreeNode(value);
currNode = currNode.right;
}
return dummyNode.right;
}
public void inorder(TreeNode node, List<Integer> res) {
if (node == null) {
return;
}
inorder(node.left, res);
res.add(node.val);
inorder(node.right, res);
}
}
53.二叉搜索树中的中序后继
关键算法:
算法思想:中序遍历。使用中序遍历的非递归方法,将根节点放到栈中,然后通过出栈的方式来保存当前节点。同时判断它的前一个节点是否是要找的节点。每次在进行下一个节点的出栈时,都会将上一个节点保存为前一个节点。
关键算法:
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
TreeNode prev = null, curr = root;
while (!stack.isEmpty() || curr != null) {
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
if (prev == p) {
return curr;
}
prev = curr;
curr = curr.right;
}
return null;
}
}
方法二:
关键算法:
算法思想:利用二叉搜索树的性质。首先根据二叉搜索树大小的性质,如果要找的节点的右节点不为空,那么右节点的第一个中序遍历的节点就是要找的节点。如果右节点为空,我们需要找到p的下一个节点,利用大小的值,从根节点开始,小于目标的值,说明p在根节点的右边,然后进入右节点。如果根节点大于目标值,说明p在根节点的左边,就要向左移动,移动的同时要保存父节点,父节点可能就是要找的节点。
关键算法:
class Solution {
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
TreeNode successor = null;
if (p.right != null) {
successor = p.right;
while (successor.left != null) {
successor = successor.left;
}
return successor;
}
TreeNode node = root;
while (node != null) {
if (node.val > p.val) {
successor = node;
node = node.left;
} else {
node = node.right;
}
}
return successor;
}
}
54.所有大于等于节点的值之和
关键算法:
算法思想:反序遍历。
关键算法:
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
if (root != null) {
convertBST(root.right);
sum += root.val;
root.val = sum;
convertBST(root.left);
}
return root;
}
}
方法二:
关键算法:
算法思想:Morris遍历。
关键算法:
class Solution {
public TreeNode convertBST(TreeNode root) {
int sum = 0;
TreeNode node = root;
while (node != null) {
if (node.right == null) {
sum += node.val;
node.val = sum;
node = node.left;
} else {
TreeNode succ = getSuccessor(node);
if (succ.left == null) {
succ.left = node;
node = node.right;
} else {
succ.left = null;
sum += node.val;
node.val = sum;
node = node.left;
}
}
}
return root;
}
public TreeNode getSuccessor(TreeNode node) {
TreeNode succ = node.right;
while (succ.left != null && succ.left != node) {
succ = succ.left;
}
return succ;
}
}
55.二叉搜索树迭代器
关键算法:
算法思想:中序遍历。
关键算法:
class BSTIterator {
private int idx;
private List<Integer> arr;
public BSTIterator(TreeNode root) {
idx = 0;
arr = new ArrayList<Integer>();
inorderTraversal(root, arr);
}
public int next() {
return arr.get(idx++);
}
public boolean hasNext() {
return idx < arr.size();
}
private void inorderTraversal(TreeNode root, List<Integer> arr) {
if (root == null) {
return;
}
inorderTraversal(root.left, arr);
arr.add(root.val);
inorderTraversal(root.right, arr);
}
}
方法二:
关键算法:
算法思想:迭代。使用栈来模拟保存的数据。
关键算法:
class BSTIterator {
private TreeNode cur;
private Deque<TreeNode> stack;
public BSTIterator(TreeNode root) {
cur = root;
stack = new LinkedList<TreeNode>();
}
public int next() {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
int ret = cur.val;
cur = cur.right;
return ret;
}
public boolean hasNext() {
return cur != null || !stack.isEmpty();
}
}
56.二叉搜索树中两个节点之和
关键算法:
算法思想:深度优先搜索 + 哈希表。
关键算法:
class Solution {
Set<Integer> set = new HashSet<Integer>();
public boolean findTarget(TreeNode root, int k) {
if (root == null) {
return false;
}
if (set.contains(k - root.val)) {
return true;
}
set.add(root.val);
return findTarget(root.left, k) || findTarget(root.right, k);
}
}
方法二:
关键算法:
算法思想:广度优先搜索 + 哈希表。
关键算法:
class Solution {
public boolean findTarget(TreeNode root, int k) {
Set<Integer> set = new HashSet<Integer>();
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (set.contains(k - node.val)) {
return true;
}
set.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return false;
}
}
57.值和下标之差都在给定的范围内
关键算法:
算法思想:滑动窗口+有序结合。窗口每次进入一个数就会删除一个数字。在这种情况下,只要找到一个数在nums[i] - t <= x <= nums[i] + t之间就可以。
关键算法:
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
int n = nums.length;
TreeSet<Long> set = new TreeSet<Long>();
for (int i = 0; i < n; i++) { //
Long ceiling = set.ceiling((long) nums[i] - (long) t); // set中是否存在大于这里面的数
if (ceiling != null && ceiling <= (long) nums[i] + (long) t) { // 同时要小于nums[i] + t
return true;
}
set.add((long) nums[i]);
if (i >= k) { // 窗口已经满了,先移除,因为下一步会加一个数进来
set.remove((long) nums[i - k]);
}
}
return false;
}
}
58.日程表
关键算法:
算法思想:直接遍历。
关键算法:
class MyCalendar {
List<int[]> booked;
public MyCalendar() {
booked = new ArrayList<int[]>();
}
public boolean book(int start, int end) {
for (int[] arr : booked) {
int l = arr[0], r = arr[1];
if (l < end && start < r) { // 这个是三种情况的综合,必须是与的关系。
return false;
}
}
booked.add(new int[]{start, end});
return true;
}
}
59.数据流的第 K 大数值
关键算法:
算法思想:优先队列。使用PriorityQueue类存放数据,它会从小到大的存储数据,当存够k个,队头就是要找到数。如果超过了k个,则将最小的移除。
关键算法:
class KthLargest {
PriorityQueue<Integer> pq;
int k;
public KthLargest(int k, int[] nums) {
this.k = k;
pq = new PriorityQueue<Integer>();
for (int x : nums) {
add(x);
}
}
public int add(int val) {
pq.offer(val);
if (pq.size() > k) {
pq.poll();
}
return pq.peek();
}
}
60.出现频率最高的 k 个数字
关键算法:
算法思想:优先队列。
关键算法:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) { // 当个数大于里面的最小值,则将最小值换掉。
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count}); // 一直添加k个
}
}
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];
}
return ret;
}
}
61.和最小的 k 个数对
关键算法:
算法思想:优先队列。
关键算法:
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
PriorityQueue<int[]> pq = new PriorityQueue<>(k, (o1, o2)->{
return nums1[o1[0]] + nums2[o1[1]] - nums1[o2[0]] - nums2[o2[1]];
});
List<List<Integer>> ans = new ArrayList<>();
int m = nums1.length;
int n = nums2.length;
for (int i = 0; i < Math.min(m, k); i++) { // 因为i不会超过m和k的最小值,最多也就kg
pq.offer(new int[]{i,0}); // 先将第一个数组中所有数和第二个数组的第一个数的索引放到pq中。
}
while (k-- > 0 && !pq.isEmpty()) {
int[] idxPair = pq.poll();
List<Integer> list = new ArrayList<>();
list.add(nums1[idxPair[0]]);
list.add(nums2[idxPair[1]]);
ans.add(list);
if (idxPair[1] + 1 < n) {
pq.offer(new int[]{idxPair[0], idxPair[1] + 1}); // 开始第二个数组的遍历。只存k个。
}
}
return ans;
}
}
62.实现前缀树
关键算法:
算法思想:套娃。每一个Trie类型中有一个具有26个Trie类型的数组,这样只要插入的字符出现过,就能在字典中找到它的前缀。使用 isEnd来表示字符是不是结束。
关键算法:
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) { // 只保存所有插入的字符中第i位没有过的字符,如只有acx和abx,则第一个数组中只有a,第二个数组中有b,c,第三个数组中只有x。将所有插入的字符都作为了前缀的字典。
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) { // 判断这个字在不在前缀树中
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) { // 判断这个是不是字符的前缀
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}
63.替换单词
关键算法:
算法思想:哈希集合。将字典中的字符串保存在set中,然后一次遍历每个词,并取每个前缀,如果字典中包含了前缀,则将这个单词替换。
关键算法:
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Set<String> dictionarySet = new HashSet<String>();
for (String root : dictionary) {
dictionarySet.add(root);
}
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) {
String word = words[i];
for (int j = 0; j < word.length(); j++) {
if (dictionarySet.contains(word.substring(0, 1 + j))) {
words[i] = word.substring(0, 1 + j);
break;
}
}
}
return String.join(" ", words);
}
}
方法二:
关键算法:
算法思想:字典树。
关键算法:
class Solution {
public String replaceWords(List<String> dictionary, String sentence) {
Trie trie = new Trie();
for (String word : dictionary) {
Trie cur = trie;
for (int i = 0; i < word.length(); i++) { // 将dic中的字符串放如字典中
char c = word.charAt(i);
cur.children.putIfAbsent(c, new Trie());
cur = cur.children.get(c);
}
cur.children.put('#', new Trie()); // 结束字符
}
String[] words = sentence.split(" ");
for (int i = 0; i < words.length; i++) { // 对每个字进行检测前缀。
words[i] = findRoot(words[i], trie);
}
return String.join(" ", words);
}
public String findRoot(String word, Trie trie) { // trie为根
StringBuffer root = new StringBuffer();
Trie cur = trie;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.children.containsKey('#')) { // 它不是结束字符,则返回保存的前缀
return root.toString();
}
if (!cur.children.containsKey(c)) { // 不包含这个字符,说明前缀不在,返回这个单词
return word;
}
root.append(c); // 包含这个单词,将单词保存,以便返回。
cur = cur.children.get(c);
}
return root.toString();
}
}
class Trie {
Map<Character, Trie> children;
public Trie() {
children = new HashMap<Character, Trie>();
}
}
64.神奇的字典
关键算法:
算法思想:枚举每个字典中的字符串并判断。对搜索的单词进行和字典中的每个词进行判断,如果只有一个字符不同,那么返回true,如果大于一个,则跳过,进入字典中下一个。
关键算法:
class MagicDictionary {
private String[] words;
public MagicDictionary() {
}
public void buildDict(String[] dictionary) {
words = dictionary;
}
public boolean search(String searchWord) {
for (String word : words) {
if (word.length() != searchWord.length()) {
continue;
}
int diff = 0;
for (int i = 0; i < word.length(); ++i) {
if (word.charAt(i) != searchWord.charAt(i)) {
++diff;
if (diff > 1) {
break;
}
}
}
if (diff == 1) {
return true;
}
}
return false;
}
}
65.最短的单词编码
关键算法:
算法思想:存储后缀。
关键算法:
class Solution {
public int minimumLengthEncoding(String[] words) {
Set<String> good = new HashSet<String>(Arrays.asList(words));
for (String word: words) {
for (int k = 1; k < word.length(); ++k) {
good.remove(word.substring(k)); // 如果这个单词出现在这个单词的字串里,则将它从set中删除。
}
}
int ans = 0;
for (String word: good) { // 遍历set,取出每个单词,并计算它的长度+1。
ans += word.length() + 1;
}
return ans;
}
}
66.单词之和
关键算法:
算法思想:暴力扫描。
关键算法:
class MapSum {
Map<String, Integer> map;
public MapSum() {
map = new HashMap<>();
}
public void insert(String key, int val) {
map.put(key,val);
}
public int sum(String prefix) {
int res = 0;
for (String s : map.keySet()) {
if (s.startsWith(prefix)) {
res += map.get(s);
}
}
return res;
}
}
67.最大的异或
关键算法:
算法思想:哈希表。
关键算法:
class Solution {
// 最高位的二进制位编号为 30
static final int HIGH_BIT = 30;
public int findMaximumXOR(int[] nums) {
int x = 0;
for (int k = HIGH_BIT; k >= 0; --k) {
Set<Integer> seen = new HashSet<Integer>();
// 将所有的 pre^k(a_j) 放入哈希表中
for (int num : nums) {
// 如果只想保留从最高位开始到第 k 个二进制位为止的部分
// 只需将其右移 k 位
seen.add(num >> k);
}
// 目前 x 包含从最高位开始到第 k+1 个二进制位为止的部分
// 我们将 x 的第 k 个二进制位置为 1,即为 x = x*2+1
int xNext = x * 2 + 1;
boolean found = false;
// 枚举 i
for (int num : nums) {
if (seen.contains(xNext ^ (num >> k))) {
found = true;
break;
}
}
if (found) {
x = xNext;
} else {
// 如果没有找到满足等式的 a_i 和 a_j,那么 x 的第 k 个二进制位只能为 0
// 即为 x = x*2
x = xNext - 1;
}
}
return x;
}
}