文章有点长,推荐收藏
文章目录
leetcode 刷题的正确打开方式
一、 线性数据结构刷题思路
数组-> 链表-> 哈希表->字符串->栈与队列
二、 数组系列
1、二分查找
从最简单的二分开始
- leetcode 704. 二分查找
code :
class Solution {
public int search(int[] nums, int target) {
int pivot, left = 0, right = nums.length - 1;
while (left <= right) {
pivot = left + right >> 1;
if (nums[pivot] == target) {
return pivot;
} else if (nums[pivot] < target) {
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return -1;
}
}
- leetcode 35.搜索插入位置
code:
class Solution {
public int searchInsert(int[] nums, int target) {
int pivot, left = 0, right = nums.length - 1;
while (left <= right) {
pivot = left + right >> 1;
if (nums[pivot] == target) {
return pivot;
} else if (nums[pivot] < target) {
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return right + 1;
}
}
- leetcode 34. 在排序数组中查找元素的第一个和最后一个位置
code:
class Solution {
// 根据二分,找到的元素向左向右遍历
public int[] searchRange(int[] nums, int target) {
int pos = binarySearch(nums, target);
if (pos == -1) return new int[]{-1, -1};
int i, j, temp = pos;
while (nums[temp] == target) {
temp --;
if (temp < 0) break;
}
i = temp + 1;
while (nums[pos] == target) {
pos ++;
if (pos > nums.length - 1) break;
}
j = pos - 1;
return new int[]{i, j};
}
// 经典的二分查找
public int binarySearch(int[] nums, int target) {
int pivot, left = 0, right = nums.length - 1;
while (left <= right) {
pivot = left + right >> 1;
if (nums[pivot] == target) {
return pivot;
} else if (nums[pivot] < target) {
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return -1;
}
}
- leetcode 69. x 的平方根
code:
class Solution {
public int mySqrt(int x) {
int pivot, left = 0, right = x, ans = -1;
while (left <= right) {
pivot = left + right >> 1;
if ((long) pivot * pivot <= x) {
ans = pivot;
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return ans;
}
}
- leetcode 367. 有效的完全平方数
code:
class Solution {
public boolean isPerfectSquare(int num) {
long i = binarySearch(num);
if ((long)i*i == num) return true;
return false;
}
public long binarySearch(int x) {
long pivot, left = 0, right = x, ans = -1;
while (left <= right) {
pivot = left + right >> 1;
long mul = (long)(pivot * pivot);
if (mul <= x) {
ans = pivot;
left = pivot + 1;
} else {
right = pivot - 1;
}
}
return ans;
}
}
2、移除数组元素(双指针等等)
核心思路:双指针
- leetcode 27. 移除元素
双指针:
class Solution {
public int removeElement(int[] nums, int val) {
int n = nums.length;
int left = 0;
for (int right = 0; right < n; right++) {
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
}
return left;
}
}
- leetcode 26. 删除有序数组中的重复项
双指针
class Solution {
public int removeDuplicates(int[] nums) {
int len = nums.length;
int left = 0;
for (int right = 1; right < len; right ++) {
if (nums[right] != nums[left]) {
nums[++ left] = nums[right];
}
}
// nums = Arrays.copyOfRange(nums, 0, left);
return left + 1;
}
}
- leetcode 283. 移动零
双指针
class Solution {
public void moveZeroes(int[] nums) {
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;
left ++;
}
}
}
}
- leetcode 844. 比较退格的字符串
重构字符串
class Solution {
public boolean backspaceCompare(String S, String T) {
return build(S).equals(build(T));
}
public String build(String str) {
StringBuffer ret = new StringBuffer();
int length = str.length();
for (int i = 0; i < length; ++i) {
char ch = str.charAt(i);
if (ch != '#') {
ret.append(ch);
} else {
if (ret.length() > 0) {
ret.deleteCharAt(ret.length() - 1);
}
}
}
return ret.toString();
}
}
双指针
class Solution {
public boolean backspaceCompare(String S, String T) {
int i = S.length() - 1, j = T.length() - 1;
int skipS = 0, skipT = 0;
while (i >= 0 || j >= 0) {
while (i >= 0) {
if (S.charAt(i) == '#') {
skipS++;
i--;
} else if (skipS > 0) {
skipS--;
i--;
} else {
break;
}
}
while (j >= 0) {
if (T.charAt(j) == '#') {
skipT++;
j--;
} else if (skipT > 0) {
skipT--;
j--;
} else {
break;
}
}
if (i >= 0 && j >= 0) {
if (S.charAt(i) != T.charAt(j)) {
return false;
}
} else {
if (i >= 0 || j >= 0) {
return false;
}
}
i--;
j--;
}
return true;
}
}
- leetcode 977. 有序数组的平方
双指针 (左右边界指针扫描,归并逆序存进新数组)
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] res = new int[n];
for (int left = 0, right = n - 1, pos = n - 1; left <= right ;) {
int l = nums[left] * nums[left];
int r = nums[right] * nums[right];
if (l <= r) {
res[pos --] = r;
right --;
} else {
res[pos --] = l;
left ++;
}
}
return res;
}
}
3、滑动窗口系列(滑动窗口+固定窗口)
滑动窗口系列:
- leetcode 209.长度最小的子数组
滑动窗口
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int left = 0, sum = 0, ans = Integer.MAX_VALUE;
for (int right = 0; right < n; right ++) {
sum += nums[right];
while (sum >= target) {
ans = Math.min(ans, right - left + 1);
sum -= nums[left];
left ++;
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
- leetcode 904. 水果成篮
普通的滑动窗口:
class Solution {
public int totalFruit(int[] tree) {
int n = tree.length;
if (n == 0) return 0;
if (n == 1) return 1;
int left = 0, ans = 0, sum = 0;
HashMap<Integer, Integer> basket = new HashMap<>();
for (int right = 0; right < n; right ++) {
basket.put(tree[right], basket.getOrDefault(tree[right], 0) + 1);
sum = 0;
while (basket.size() > 2) {
basket.put(tree[left], basket.get(tree[left]) - 1);
if (basket.get(tree[left]) == 0) basket.remove(tree[left]);
left ++;
}
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
史上最快滑动窗口:
class Solution {
public int totalFruit(int[] tree) {
int maxLen = 0, len = tree.length;
int start = 0, end = 0;
int one = tree[end], two = 0;
// 找第一种果树
while(end < len && tree[end] == one) end++;
if(end == len) return len;
// 找第二种果树
two = tree[end++];
// 计算滑动窗口长度
for(; end < len; end++){
if(tree[end] != one && tree[end] != two){
// 计算当前滑动窗口的长度
maxLen = Math.max(maxLen, end - start);
// 更新两种果树类型
one = tree[end - 1];
two = tree[end];
// 更新滑动窗口的左边界
start = end - 1;
// 尽量向左延伸滑动窗口的左边界
while(tree[start - 1] == one)
start--;
}
}
return Math.max(maxLen, end - start);
}
}
- leetcode 3. 无重复字符的最长子串
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int left = 0, ans = 0;
int[] hash = new int[256];
for (int right = 0; right < n; right ++) {
int index = s.charAt(right) - ' ';
hash[index] ++;
while (hash[index] > 1) {
hash[s.charAt(left) - ' '] --;
left ++;
}
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
- leetcode 1208. 尽可能的使字符串相等
class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int n = s.length();
if (n == 0) return 0;
int left = 0, ans = 0, sum = 0;
for (int right = 0; right < n; right ++) {
sum += Math.abs(s.charAt(right) - t.charAt(right));
while (sum > maxCost) {
sum -= Math.abs(s.charAt(left) - t.charAt(left));
left ++;
}
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
稍稍美化一下代码:
class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int n = s.length();
if (n == 0) return 0;
int[] diff = new int[n];
for (int i = 0; i < n; i ++) {
diff[i] = Math.abs(s.charAt(i) - t.charAt(i));
}
int left = 0, ans = 0, sum = 0;
for (int right = 0; right < n; right ++) {
sum += diff[right];
while (sum > maxCost) {
sum -= diff[left];
left ++;
}
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
固定窗口系列:
- leetcode 567.字符串的排列
class Solution {
public boolean checkInclusion(String s1, String s2) {
int[] hash1 = new int[256];
int[] hash2 = new int[256];
int size1 = s1.length();
int size2 = s2.length();
if (size1 > size2) return false;
int left = 0;
for (int i = 0; i < size1; i ++) {
hash1[s1.charAt(i) - ' '] ++;
hash2[s2.charAt(i) - ' '] ++;
}
if (Arrays.equals(hash1, hash2)) return true;
for (int right = left + size1; right < size2; right ++) {
hash2[s2.charAt(right) - ' '] ++;
hash2[s2.charAt(left) - ' '] --;
if (Arrays.equals(hash1, hash2)) return true;
left ++;
}
return false;
}
}
- leetcode 1052. 爱生气的书店老板
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int[] antiGrumy = new int[n];
int left = 0, sum = 0;
if (customers.length <= minutes) {
for (int i = 0; i < customers.length; i ++) {
sum += customers[i];
}
return sum;
}
for (int i = 0; i < n; i ++) {
if (grumpy[i] == 0) sum += customers[i];
if (grumpy[i] == 1 && i < minutes) sum += customers[i];
}
int ans = sum;
for (int right = minutes; right < n; right ++) {
if (grumpy[right] == 1) sum += customers[right];
if (grumpy[left] == 1) sum -= customers[left];
ans = Math.max(ans, sum);
left ++;
}
return ans;
}
}
代码美化后:
class Solution {
public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
int n = customers.length;
int left = 0, sum = 0;
if (customers.length <= minutes) {
for (int i = 0; i < customers.length; i ++) {
sum += customers[i];
}
return sum;
}
// sum表示所有正常情况顾客数量
for (int i = 0; i < n; i ++) {
if (grumpy[i] == 0) sum += customers[i];
}
// 开启技能!如果grumy = 0,则加上
for (int i = 0; i < minutes; i ++) {
sum += customers[i] * (grumpy[i] ^ 0);
}
int ans = sum;
for (int right = minutes; right < n; right ++) {
sum += customers[right] * grumpy[right];
sum -= customers[left] * grumpy[left];
ans = Math.max(ans, sum);
left ++;
}
return ans;
}
}
4、旋转的二维数组
- leetcode 59. 螺旋矩阵 II
缩小边界法:
边遍历,边更改边界
class Solution {
public int[][] generateMatrix(int n) {
int[][] matrix = new int[n][n];
int up = 0, down = n - 1, left = 0, right = n - 1;
int i = 0, j = 0;
for (int k = 1 ; k <= n * n;) {
for (j = left; j <= right; j ++) {
matrix[up][j] = k ++;
}
up ++;
for (i = up; i <= down; i ++) {
matrix[i][right] = k ++;
}
right --;
for (j = right; j >= left; j --) {
matrix[down][j] = k ++;
}
down --;
for (i = down; i >=up ; i --) {
matrix[i][left] = k ++;
}
left ++;
}
return matrix;
}
}
- leetcode 剑指 Offer 29. 顺时针打印矩阵
缩小边界法
class Solution {
public int[] spiralOrder(int[][] matrix) {
int n = matrix.length;
// 如果不加这个判断,遇到空数组会导致nullpointerexception
if (n == 0) return new int[]{};
int m = matrix[0].length;
int[] ans = new int[n * m];
int up = 0, left = 0, down = n - 1, right = m - 1;
for (int k = 0; k < m * n;) {
for (int j = left; j <= right; j ++) {
// 如果不加这个判断会导致nullpointerexception
if (k == m * n) break;
ans[k ++] = matrix[up][j];
}
up ++;
for (int i = up; i <= down; i ++) {
if (k == m * n) break;
ans[k ++] = matrix[i][right];
}
right --;
for (int j = right; j >= left; j --) {
if (k == m * n) break;
ans[k ++] = matrix[down][j];
}
down --;
for (int i = down; i >= up; i --) {
if (k == m * n) break;
ans[k ++] = matrix[i][left];
}
left ++;
}
return ans;
}
}
三、链表系列
1、dummyHead的巧妙应用
- leetcode 203.移除链表元素
体会一下下面代码中 因为引入了 dummyHead 而不用对 头节点 进行特殊处理的便利, 思想有点类似于哨兵原理。
/**
* 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 removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode(0, head);
ListNode prev = dummyHead, curr = head;
while (curr != null) {
if (curr.val == val) {
prev.next = curr.next;
curr = curr.next;
} else {
prev = prev.next;
curr = curr.next;
}
}
return dummyHead.next;
}
}
2、设计链表
考察基本功,在了解链表都操作后,进行相关的数据结构设计
- leetcode 707. 设计链表
使用双向链表设计一个链表操作
class MyLinkedList {
private ListNode dummyHead;
private ListNode tail;
private int size = 0;
/** Initialize your data structure here. */
public MyLinkedList() {
tail = new ListNode(0);
dummyHead = new ListNode(0, tail);
tail.prev = dummyHead;
}
/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
public int get(int index) {
if (index < 0 || index >= size) return -1;
ListNode curr = dummyHead;
while (index -- != 0) {
curr = curr.next;
}
return curr.next.val;
}
/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
public void addAtHead(int val) {
ListNode newNode = new ListNode(val);
newNode.next = dummyHead.next;
newNode.prev = dummyHead;
newNode.prev.next = newNode;
newNode.next.prev = newNode;
size ++;
}
/** Append a node of value val to the last element of the linked list. */
public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
newNode.next = tail;
newNode.prev = tail.prev;
newNode.next.prev = newNode;
newNode.prev.next = newNode;
size ++;
}
/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
public void addAtIndex(int index, int val) {
if (index <= 0) {
addAtHead(val);
}else if (index > size) {
addAtTail(val);
}else {
ListNode curr = dummyHead;
ListNode newNode = new ListNode(val);
while (index -- != 0) {
curr = curr.next;
}
newNode.next = curr.next;
newNode.prev = curr;
newNode.next.prev = newNode;
newNode.prev.next = newNode;
}
size ++;
}
/** Delete the index-th node in the linked list, if the index is valid. */
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) return ;
ListNode curr = dummyHead;
while (index -- != 0) {
curr = curr.next;
}
curr = curr.next;
curr.next.prev = curr.prev;
curr.prev.next = curr.next;
size --;
}
}
class ListNode {
int val;
ListNode next;
ListNode prev;
ListNode(int val) {this.val = val;}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
3、反转链表系列
- leetcode 206. 反转链表
无敌的板子
/**
* 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 reverseList(ListNode head) {
ListNode curr = head;
ListNode prev = null;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
面试拔高:k个节点为一组的链表反转
可以参看我之前的一篇文章
public static ListNode reverseListByNumber(ListNode head, int k) {
ListNode prev = null;
ListNode curr = head;
ListNode first = null;
ListNode dummyHead = new ListNode(0);
ListNode conn = dummyHead;
while (curr != null) {
int count = k;
first = curr;
while (count-- != 0 && curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
conn.next = prev;
conn = first;
}
conn.next = null;
return dummyHead.next;
}
- leetcode 24. 两两交换链表中的节点
/**
* 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 swapPairs(ListNode head) {
if (head == null) return null;
ListNode dummyHead = new ListNode(0, head);
ListNode prev = dummyHead;
ListNode curr = head;
while (prev.next != null && prev.next.next != null) {
ListNode temp = prev.next.next.next;
curr = prev.next;
prev.next = prev.next.next;
prev.next.next = curr;
curr.next = temp;
prev = prev.next.next;
}
return dummyHead.next;
}
}
4、高效操作链表
leetcode 19. 删除链表倒数第n个节点
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(0, head);
ListNode first = head, second = dummyHead;
while (n -- != 0) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummyHead.next;
}
}
5、判断链表有无交叉
面试题 02.07. 链表相交
朴素实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
HashMap<ListNode, Integer> hashmap = new HashMap<>();
while (headA != null) {
hashmap.put(headA, 1);
headA = headA.next;
}
while (headB != null) {
if (hashmap.get(headB) != null) {
return headB;
}
headB = headB.next;
}
return null;
}
}
无敌优化思路
A长度为 a, B长度为b, 假设存在交叉点,此时 A到交叉点距离为 c, 而B到交叉点距离为d
后续交叉后长度是一样的,那么就是 a-c = b-d => a+d = b+c
这里意味着只要分别让A和B额外多走一遍B和A,那么必然会走到交叉,注意这里边缘情况是,大家都走到null依然没交叉,那么正好返回null即可
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pa = headA, pb = headB;
while (pa != pb) {
pa = pa == null ? headB : pa.next;
pb = pb == null ? headA : pb.next;
}
return pa;
}
}
6、判断环形链表问题合集
判断链表是否有环
如过有环,链表的环入口在哪里
- leetcode 142. 环形链表 II
思路讲解:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) return null;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇后同时从 head 以及相遇点 遍历,相遇点即时环入口
if (slow == fast) {
ListNode p1 = head;
ListNode p2 = slow;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1; // 得到环的入口
}
}
return null;
}
}
四、哈希表问题大全
1、简单模拟哈希表
- leetcode 242. 有效的字母异位词
简单的模拟哈希
利用ascii码锁住字符在数组的位置
class Solution {
public boolean isAnagram(String s, String t) {
int n = s.length();
if (n != t.length()) return false;
int[] hash = new int[128];
for (int i = 0; i < n; i ++) {
char c = s.charAt(i);
hash[c - ' '] ++;
}
for (int i = 0; i < n; i ++) {
char c = t.charAt(i);
if (hash[c - ' '] <= 0) return false;
hash[c - ' '] --;
}
return true;
}
}
- leetcode 349. 两个数组的交集
依然是简单的模拟哈希
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int n1 = nums1.length, n2 = nums2.length;
if (n1 == 0 || n2 == 0) return new int[0];
int[] hash = new int[1000];
int[] ans = new int[1000];
int k = 0;
for (int i = 0; i < n1; i ++) {
if (hash[nums1[i]] == 0) hash[nums1[i]] ++;
}
for (int i = 0; i < n2; i ++) {
if (hash[nums2[i]] == 1) {
ans[k ++] = nums2[i];
hash[nums2[i]] --;
}
}
return Arrays.copyOf(ans, k);
}
}
- leetcode 202. 快乐数
并不是所有的题目都可以用模拟hash 的方法
例如这道题
如果我们还用模拟法的话会是什么样子呢?
class Solution {
public boolean isHappy(int n) {
int[] hash = new int[10000];
while (n != 1) {
hash[n] ++;
n = getNextNumber(n);
if (hash[n] == 1) return false;
}
return true;
}
public int getNextNumber(int x) {
int res = 0;
while (x != 0) {
res += (x % 10) * (x % 10);
x /= 10;
}
return res;
}
}
当遇到这种情况下我们该怎么做呢?
有人可能会说,扩大数组的范围呀,显然这样是不划算的,而且会导致内存溢出,这个时候我们就只能用一下java为我们准备好的工具啦!
HashMap
class Solution {
public boolean isHappy(int n) {
HashMap<Integer, Integer> hashmap = new HashMap<>();
while (n != 1) {
hashmap.put(n, 1);
n = getNextNumber(n);
if (hashmap.containsKey(n)) return false;
}
return true;
}
public int getNextNumber(int x) {
int res = 0;
while (x != 0) {
res += (x % 10) * (x % 10);
x /= 10;
}
return res;
}
}
通过啦! 以后再刷题的道路上也有很多地方需要用到HashMap这样很棒棒的工具呢!
2、利用HashMap解决问题
- leetcode 1. 两数之和
利用HashMap来实现 O(1)搜索:
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> hashmap = new HashMap<>();
int n = nums.length;
for (int i = 0; i < n; i ++) {
if (hashmap.containsKey(target - nums[i])) {
return new int[]{hashmap.get(target - nums[i]), i};
}
hashmap.put(nums[i], i);
}
return new int[0];
}
}
- leetcode 454. 四数相加 II
解题思路:
首先看到这道题的第一反应就是分组 + 哈希表; 把暴力 n4 的时间复杂度降到 n2;然后进行实现,代码如下,提交发现用了142ms…,后面我们进行优化。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int n = nums1.length;
HashMap<Integer, Integer> hashmap = new HashMap<>();
int ans = 0;
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
hashmap.put(nums1[i] + nums2[j], hashmap.getOrDefault(nums1[i] + nums2[j], 0) + 1);
}
}
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
if (hashmap.containsKey( - nums3[i] - nums4[j])) {
ans += hashmap.get(- nums3[i] - nums4[j]);
}
}
}
return ans;
}
}
优化思路:
优化个锤子,看了大家的题解,发现执行最快的是手写hashmap功能的大佬代码在下面,看看热闹就好。。。在真正有项目需要的时候一般我们不需要自己造轮子来使性能达到登峰造极。
class Solution {
private static class Node {
int value;
int count;
Node next;
public Node(int value) {
this.value = value;
this.count = 1;
}
public Node(int value, Node next) {
this.value = value;
this.count = 1;
this.next = next;
}
}
private static class Map {
Node[] table;
public Map(int initalCapacity) {
if (initalCapacity < 16) {
initalCapacity = 16;
} else {
initalCapacity = Integer.highestOneBit(initalCapacity - 1) << 1;
}
table = new Node[initalCapacity];
}
// 拷贝的HashMap的hash方法
private int hash(int value) {
if (value < 0) {
value = -value;
}
int h;
return (value == 0) ? 0 : (h = value) ^ (h >>> 16);
}
public void put(int value) {
int tableIndex = hash(value) & table.length - 1;
Node head = table[tableIndex];
if (head == null) {
table[tableIndex] = new Node(value);
return;
}
Node cur = head;
while (cur != null) {
if (cur.value == value) {
cur.count++;
return;
}
cur = cur.next;
}
// 头插法
table[tableIndex] = new Node(value, head);
}
public int getCount(int value) {
int tableIndex = hash(value) & table.length - 1;
Node head = table[tableIndex];
if (head == null) {
return 0;
}
Node cur = head;
while (cur != null) {
if (cur.value == value) {
return cur.count;
}
cur = cur.next;
}
return 0;
}
}
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
// 避免扩容, 初始化一个最大初始容量
Map abMap = new Map(A.length * B.length);
for (int a : A) {
for (int b : B) {
abMap.put(a + b);
}
}
int res = 0;
for (int c : C) {
for (int d : D) {
res += abMap.getCount(-c - d);
}
}
return res;
}
}
- leetcode 383. 赎金信
解题思路:
首先看到这道题,有字符串,判断能否由子串进行拼凑的时候,一看就知道是要用哈希表来进行O(1)的查找与更新了,这里我们先用模拟哈希的方式进行一个简单的实现
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int len = ransomNote.length(), n = magazine.length();
if (len > n) return false;
int[] hash = new int[128];
for (int i = 0; i < n; i ++) {
char c = magazine.charAt(i);
hash[c - ' '] ++;
}
for (int i = 0; i < len; i ++) {
char c = ransomNote.charAt(i);
if (hash[c - ' '] == 0) return false;
hash[c - ' '] --;
}
return true;
}
}
效果还不错,让我们来看看执行速度最快的大佬:
分析:这个大佬边界条件多判断了两个字符串是否相同,就可以直接返回, 其次 因为题目中有提及字符串中只有26个小写字母,无疑可以进行更加细致的哈希设置。代码如下:
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
if (ransomNote.equals(magazine)) {
return true;
}
int[] chars = new int[26];
int ransomLen = ransomNote.length();
for (int i = 0; i < ransomLen; i++) {
chars[ransomNote.charAt(i) - 97]++;
}
int Mlen = magazine.length();
for (int i = 0; i < Mlen && ransomLen > 0; i++) {
if (chars[magazine.charAt(i) - 97] != 0) {
chars[magazine.charAt(i) - 97]--;
ransomLen--;
}
}
return ransomLen == 0;
}
}
3、哈希表无法解决的问题
也有一些问题首先就想到用hash去重,不过时间复杂度很高,让我们来看一看
- leetcode 15. 三数之和
解题思路:
题意已经十分清楚了,不允许有重复的元祖出现,这个时候大家就可能会想,用hash来去重,这是一个思路。在构造哈希的前提就是要去找到满足条件的元祖。
不过找满足条件的元祖的时候使用到三重循环,会使得时间复杂度n3导致超时。
我们换一种思路,如果想去重还有一种很好的方法:排序
在排序后在利用双指针进行 遍历,就豁然开朗了。
双指针代码如下: (最快执行速度)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
List<List<Integer>> ans = new ArrayList<>();
if (n < 3) return ans;
Arrays.sort(nums);
for (int i = 0; i < n; i ++) {
// 最小的数字大于0就提前返回
if (nums[i] > 0) return ans;
// 如果nums[i]遍历过,每次都去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1, right = n - 1;
if (left >= right) continue;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left ++;
} else if (sum > 0) {
right --;
} else {
ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 已经完成添加后的元素,要去掉重复的部分
while (left < right && nums[left] == nums[left + 1]) left ++;
// 已经完成添加后的元素,要去掉重复的部分
while (left < right && nums[right] == nums[right - 1]) right --;
left ++;
right --;
}
}
}
return ans;
}
}
- leetcode 18. 四数之和
思路和上面一样
都是将高阶复杂度通过双指针降维(这里 n4 -> n3)
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
int n = nums.length;
if (n < 4) return ans;
Arrays.sort(nums);
for (int i = 0; i < n; i ++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < n; j ++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
// 这段剪支代码不知道为什么不可以过样例,希望有知道的大佬帮我解答一下,谢谢啦~
// if (nums[i] + nums[j] > target) return ans;
int left = j + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum < target) {
left ++;
} else if (sum > target) {
right --;
} else {
ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left ++;
while (left < right && nums[right] == nums[right - 1]) right --;
left ++;
right --;
}
}
}
}
return ans;
}
}
五、字符串问题合集
对于字符串的处理,可以简明扼要的分为一下几种:
双指针、KMP
1、什么时候使用双指针
- leetcode 344. 反转字符串
解题思路
这种头尾双向遍历的模式,我们可以直接采用双指针进行操作
class Solution {
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while (left <= right) {
char tmp = s[left];
s[left ++] = s[right];
s[right --] = tmp;
}
}
}
- leetcode 541. 反转字符串 II
解题思路
只需要便利的时候将遍历变量 += 2*k,并在循环内使用双指针即可。注意判断边际条件。
code
class Solution {
public String reverseStr(String s, int k) {
int n = s.length();
char[] newString = s.toCharArray();
for (int i = 0; i < n; i += 2 * k ) {//>= n ? n : i + 2 * k ) {
int left = i, right = Math.min(i + k - 1, n - 1); // i + k - 1 >= n? n - 1 : i + k - 1;
while (left <= right) {
char temp = newString[left];
newString[left ++] = newString[right];
newString[right --] = temp;
}
}
return new String(newString);
}
}
- 剑指 Offer 05. 替换空格
解题思路一:(朴素无脑)
class Solution {
public String replaceSpace(String s) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < s.length(); i ++) {
char c = s.charAt(i);
if(c == ' ') sb.append("%20");
else sb.append(c);
}
return sb.toString();
}
}
甚至可以:
class Solution {
public String replaceSpace(String s) {
return s.replace(" ","%20");
}
}
解体思路二:双指针
如果想把这道题目做到极致,就不要只用额外的辅助空间了!
首先扩充数组到每个空格替换成"%20"之后的大小。
然后从后向前替换空格。
code:
怪麻烦的感兴趣的小伙伴自己实现一下,顺便发在评论区~
leetcode 151. 翻转字符串里的单词
朴素解体思路
简单粗暴
class Solution {
public String reverseWords(String s) {
// 除去开头和末尾的空白字符
s = s.trim();
// 正则匹配连续的空白字符作为分隔符分割
List<String> wordList = Arrays.asList(s.split("\\s+"));
Collections.reverse(wordList);
return String.join(" ", wordList);
}
}
双指针,高手手写思路
class Solution {
/**
* 不使用Java内置方法实现
* <p>
* 1.去除首尾以及中间多余空格
* 2.反转整个字符串
* 3.反转各个单词
*/
public String reverseWords(String s) {
// System.out.println("ReverseWords.reverseWords2() called with: s = [" + s + "]");
// 1.去除首尾以及中间多余空格
StringBuilder sb = removeSpace(s);
// 2.反转整个字符串
reverseString(sb, 0, sb.length() - 1);
// 3.反转各个单词
reverseEachWord(sb);
return sb.toString();
}
private StringBuilder removeSpace(String s) {
// System.out.println("ReverseWords.removeSpace() called with: s = [" + s + "]");
int start = 0;
int end = s.length() - 1;
while (s.charAt(start) == ' ') start++;
while (s.charAt(end) == ' ') end--;
StringBuilder sb = new StringBuilder();
while (start <= end) {
char c = s.charAt(start);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;
}
// System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
return sb;
}
/**
* 反转字符串指定区间[start, end]的字符
*/
public void reverseString(StringBuilder sb, int start, int end) {
// System.out.println("ReverseWords.reverseString() called with: sb = [" + sb + "], start = [" + start + "], end = [" + end + "]");
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
// System.out.println("ReverseWords.reverseString returned: sb = [" + sb + "]");
}
private void reverseEachWord(StringBuilder sb) {
int start = 0;
int end = 1;
int n = sb.length();
while (start < n) {
while (end < n && sb.charAt(end) != ' ') {
end++;
}
reverseString(sb, start, end - 1);
start = end + 1;
end = start + 1;
}
}
}
比较推荐的实现方式效率高,代码清晰。
class Solution {
public String reverseWords(String s) {
StringBuilder res = new StringBuilder();
String[] arrs = s.trim().split(" ");
int len = arrs.length;
String arr;
for(int i = 0; i < len; i ++){
arr = arrs[len - i - 1];
if(!"".equals(arr)){
res.append(arr);
res.append(i == len - 1 ? "" : " ");
}
}
return res.toString();
}
}
2、并不是所有的字符串操作都要双指针
- 剑指 Offer 58 - II. 左旋转字符串
朴素解法:
class Solution {
public String reverseLeftWords(String s, int n) {
int len = s.length();
if (n >= len) return s;
StringBuilder sb = new StringBuilder();
for (int i = n; i < len; i ++) {
sb.append(s.charAt(i));
}
for (int i = 0;i < n; i ++) {
sb.append(s.charAt(i));
}
return sb.toString();
}
}
美化一下代码:(效率低了一丢丢)
class Solution {
public String reverseLeftWords(String s, int n) {
int len = s.length();
if (n >= len) return s;
StringBuilder sb = new StringBuilder();
for (int i = n; i < n + len; i ++) {
sb.append(s.charAt(i % len));
}
return sb.toString();
}
}
简单粗暴:
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n, s.length()) + s.substring(0, n);
}
}
class Solution {
public String reverseLeftWords(String s, int n) {
int len = s.length();
if (n >= len) return s;
StringBuilder sb = new StringBuilder();
for (int i = n; i < n + len; i ++) {
sb.append(s.charAt(i % len));
}
return sb.toString();
}
}
3、KMP
六、栈和队列
1、栈和队列的互相模拟
-
- 用栈实现队列
- 用栈实现队列
class MyQueue {
Stack<Integer> fromStack;
Stack<Integer> toStack;
int size;
/** Initialize your data structure here. */
public MyQueue() {
fromStack = new Stack<>();
toStack = new Stack<>();
size = 0;
}
/** Push element x to the back of queue. */
public void push(int x) {
while (!toStack.isEmpty()) {
fromStack.push(toStack.pop());
}
fromStack.push(x);
size ++;
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
while (!fromStack.isEmpty()) {
toStack.push(fromStack.pop());
}
int ans = toStack.pop();
size --;
return ans;
}
/** Get the front element. */
public int peek() {
while (!fromStack.isEmpty()) {
toStack.push(fromStack.pop());
}
int ans = toStack.peek();
return ans;
}
/** Returns whether the queue is empty. */
public boolean empty() {
if (size == 0) return true;
// if (fromStack.isEmpty() && toStack.isEmpty()) return true;
return false;
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
-
- 用队列实现栈
- 用队列实现栈
class MyStack {
private LinkedList<Integer> queue1;
private LinkedList<Integer> queue2;
int size;
/** Initialize your data structure here. */
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
size = 0;
}
/** Push element x onto stack. */
public void push(int x) {
queue1.add(x);
size ++;
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
int top = 0;
int tempSize = size - 1;
while(!queue1.isEmpty()) {
top = queue1.remove();
if (tempSize -- > 0) queue2.add(top);
}
while (!queue2.isEmpty()) {
queue1.add(queue2.remove());
}
size --;
return top;
}
/** Get the top element. */
public int top() {
int top = 0;
while(!queue1.isEmpty()) {
top = queue1.remove();
queue2.add(top);
}
while (!queue2.isEmpty()) {
queue1.add(queue2.remove());
}
return top;
}
/** Returns whether the stack is empty. */
public boolean empty() {
if(size == 0)return true;
return false;
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
2、栈的应用
- leetcode 20. 有效的括号
利用hashmap来匹配, 中规中矩的方法,没什么含金量
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) return false;
Stack<Character> stack = new Stack<>();
HashMap<Character, Character> hashmap = new HashMap<>();
hashmap.put('{', '}');
hashmap.put('(', ')');
hashmap.put('[', ']');
for(int i = 0; i < s.length(); i ++) {
char c = s.charAt(i);
if (hashmap.containsKey(c)) stack.push(c);
else {
if (stack.isEmpty() || hashmap.get(stack.pop()) != c) return false;
// 注释的代码过AC执行速度更快
// char temp = stack.pop();
// if (temp == '}' || temp == ']' || temp == ')' ) return false;
// if (hashmap.get(temp) != c) return false;
}
}
return stack.isEmpty();
}
}
入栈的时候入对应的括号,稍微有一点点小技巧的感觉
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) return false;
Stack<Character> stack = new Stack<>();
for (int i = 0; i < n; i ++) {
char c = s.charAt(i);
if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else if (c == '(') stack.push(')');
else if (stack.isEmpty()) return false;
else if (stack.pop() != c) return false;
}
return stack.isEmpty();
}
}
- leetcode 1047. 删除字符串中的所有相邻重复项
用栈实现:
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
int n = s.length();
if (n == 1) return s;
for (int i = 0; i < n; i ++) {
char c = s.charAt(i);
if (!stack.isEmpty() && c == stack.peek()) {
stack.pop();
} else {
stack.push(c);
}
}
if (stack.isEmpty()) return "";
Stack<Character> temp = new Stack<>();
while (!stack.isEmpty()) {
temp.push(stack.pop());
}
StringBuilder sb = new StringBuilder();
while (!temp.isEmpty()) {
sb.append(temp.pop());
}
return sb.toString();
}
}
进行小小的优化:
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
StringBuilder sb = new StringBuilder();
int n = s.length();
if (n == 1) return s;
int size = 0;
for (int i = 0; i < n; i ++) {
char c = s.charAt(i);
if (!stack.isEmpty() && c == stack.peek()) {
stack.pop();
sb.delete(size - 1, size);
size --;
} else {
stack.push(c);
sb.append(c);
size ++;
}
}
return sb.toString();
}
}
再优化一下下。。。:
class Solution {
public String removeDuplicates(String S) {
StringBuffer stack = new StringBuffer();
int top = -1;
for (int i = 0; i < S.length(); ++i) {
char ch = S.charAt(i);
if (top >= 0 && stack.charAt(top) == ch) {
stack.deleteCharAt(top);
--top;
} else {
stack.append(ch);
++top;
}
}
return stack.toString();
}
}
- leetcode 150. 逆波兰表达式求值
code:
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
int n = tokens.length;
for (int i = 0; i < n; i ++) {
String s = tokens[i];
if ("+".equals(s) || "-".equals(s) || "*".equals(s) || "/".equals(s) ) {
int num2 = stack.pop();
int num1 = stack.pop();
if ("+".equals(s)) stack.push(num1 + num2);
if ("-".equals(s)) stack.push(num1 - num2);
if ("*".equals(s)) stack.push(num1 * num2);
if ("/".equals(s)) stack.push(num1 / num2);
continue;
}
stack.push(Integer.parseInt(tokens[i]));
}
return stack.pop();
}
}
官方题解:
class Solution {
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));
}
}
3、队列的应用
单调队列
- leetcode 239. 滑动窗口最大值
以下解法摘自leetcode题解中一个大佬的。
思路:
遍历数组,将 数 存放在双向队列中,并用 L,R 来标记窗口的左边界和右边界。队列中保存的并不是真的 数,而是该数值对应的数组下标位置,并且数组中的数要从大到小排序。如果当前遍历的数比队尾的值大,则需要弹出队尾值,直到队列重新满足从大到小的要求。刚开始遍历时,L 和 R 都为 0,有一个形成窗口的过程,此过程没有最大值,L 不动,R 向右移。当窗口大小形成时,L 和 R 一起向右移,每次移动时,判断队首的值的数组下标是否在 [L,R] 中,如果不在则需要弹出队首的值,当前窗口的最大值即为队首的数。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释过程中队列中都是具体的值,方便理解,具体见代码。
初始状态:L=R=0,队列:{}
i=0,nums[0]=1。队列为空,直接加入。队列:{1}
i=1,nums[1]=3。队尾值为1,3>1,弹出队尾值,加入3。队列:{3}
i=2,nums[2]=-1。队尾值为3,-1<3,直接加入。队列:{3,-1}。此时窗口已经形成,L=0,R=2,result=[3]
i=3,nums[3]=-3。队尾值为-1,-3<-1,直接加入。队列:{3,-1,-3}。队首3对应的下标为1,L=1,R=3,有效。result=[3,3]
i=4,nums[4]=5。队尾值为-3,5>-3,依次弹出后加入。队列:{5}。此时L=2,R=4,有效。result=[3,3,5]
i=5,nums[5]=3。队尾值为5,3<5,直接加入。队列:{5,3}。此时L=3,R=5,有效。result=[3,3,5,5]
i=6,nums[6]=6。队尾值为3,6>3,依次弹出后加入。队列:{6}。此时L=4,R=6,有效。result=[3,3,5,5,6]
i=7,nums[7]=7。队尾值为6,7>6,弹出队尾值后加入。队列:{7}。此时L=5,R=7,有效。result=[3,3,5,5,6,7]
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length < 2) return nums;
// 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数值按从大到小排序
LinkedList<Integer> queue = new LinkedList();
// 结果数组
int[] result = new int[nums.length-k+1];
// 遍历nums数组
for(int i = 0;i < nums.length;i++){
// 保证从大到小 如果前面数小则需要依次弹出,直至满足要求
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
queue.pollLast();
}
// 添加当前值对应的数组下标
queue.addLast(i);
// 判断当前队列中队首的值是否有效
if(queue.peek() <= i-k){
queue.poll();
}
// 当窗口长度为k时 保存当前窗口中最大值
if(i+1 >= k){
result[i+1-k] = nums[queue.peek()];
}
}
return result;
}
}
作者:hanyuhuang
链接:https://leetcode-cn.com/problems/sliding-window-maximum/solution/shuang-xiang-dui-lie-jie-jue-hua-dong-chuang-kou-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- leetcode 347. 前K个高频元素
优先队列
解题思路:
要统计元素出现频率
对频率排序
找出前K个高频元素
code:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] result = new int[k];
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
// 根据map的value值正序排,相当于一个小顶堆
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry);
if (queue.size() > k) {
queue.poll();
}
}
for (int i = k - 1; i >= 0; i--) {
result[i] = queue.poll().getKey();
}
return result;
}
}