前阵子一直在各种笔试和面试,好在也到手了两个offer,有点懒没怎么刷题,前几个月就把《剑指offer》刷了两遍,leetcode也写了快240道题了,以后刷题会慢慢减少,多写点总结,把企业手撕代码的热题认真过一遍,每篇10题
文章目录
25.k个一组翻转链表——双指针
两个重点:
- 反转函数(写烂了)
- 如何确定需要反转的一串链表
- 每段链表的前驱指针
- 每段链表的最后一个节点
public class No25 {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;//每段链表的前驱指针
ListNode end = dummy;//每段链表的最后一个节点
while (end != null) {
int i = k;
while (i > 0 && end != null) {
end = end.next;
i--;
}
//假如最后一组不够k个,就直接不用反转了
if (end == null) break;
ListNode start = pre.next;//需要反转的链表的首个节点
ListNode next = end.next;//下一段需要反转的链表的首个节点
//下面四句是链表的反转连接
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = pre;
}
return dummy.next;
}
public ListNode reverse(ListNode head) {
ListNode pre = head;
ListNode cur = null;
ListNode next = null;
while (pre != null) {
next = pre.next;
pre.next = cur;
cur = pre;
pre = next;
}
return cur;
}
}
3.无重复最长子串——hashmap
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
public class No3 {
public int lengthOfLongestSubstring_better(String s) {
if (s == null || s.length() == 0) return 0;
HashMap<Character, Integer> map = new HashMap<>();
int res = 0;
int left = 0;
for (int i = 0; i < s.length(); i++) {
if (map.containsKey(s.charAt(i))) {
//这一步很秀
left = Math.max(left, map.get(s.charAt(i)) + 1);
//考虑abba这种情况,不加left的话指针可能左移
}
map.put(s.charAt(i), i);
res = Math.max(res, i - left + 1);
}
return res;
}
}
215. 数组中的第K个最大元素——快排,优先队列
public class No215 {
//优先队列,每次弹出小的元素
public int findKthLargest(int[] nums, int k) {
if (nums == null || nums.length == 0 || nums.length < k) return -1;
Queue<Integer> queue = new PriorityQueue<>();
for (int num : nums) {
queue.offer(num);
if (queue.size() > k) {
queue.poll();
}
}
return queue.peek();
}
//没必要全部排序,利用快排,找到index=m的元素,m左边都这个元素,
//m右边都小于这个元素,若k=m+1,返回
public int findKthLargest(int[] nums, int k) {
return fastsort(nums, 0, nums.length - 1, k);
}
public int fastsort(int[] nums, int lo, int hi, int k) {
int m = partition(nums, lo, hi, k);
if (k == m + 1) {
return nums[m];
} else if (k < m + 1) {
return fastsort(nums, lo, m - 1, k);
} else {
return fastsort(nums, m + 1, hi, k);
}
}
public int partition(int[] nums, int lo, int hi, int k) {
int i = lo;
int j = hi + 1;
int pivot = nums[lo];
while (true) {
while (++i < hi && nums[i] > pivot) ;
while (--j > lo && nums[j] < pivot) ;
if (i >= j) {
break;
}
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
nums[lo] = nums[j];
nums[j] = pivot;
return j;
}
}
206. 反转链表——递归,迭代
迭代
public ListNode reverseList_v1(ListNode head) {
ListNode pre = head;
ListNode cur = null;
ListNode temp = null;
while (pre != null) {
temp = pre.next;
pre.next = cur;
cur = pre;
pre = temp;
}
return cur;
}
递归
- 递归把 head 结点拿出来,剩下的部分我们调用函数 reverseList ,
- 这样剩下的部分就逆序了,接着我们把 head 结点放到新链表的尾部就可以了。这就是整个递归的思想了。
public ListNode reverseList(ListNode head) {
//递归终止条件,一个节点的反转是它本身,如果head值null也直接返回null
if (head == null || head.next == null) return head;
ListNode cur = reverseList(head.next);//这里cur就是最后一个节点
//如果链表是 1->2->3->4->5,那么此时的cur就是5
//而head是4,head的下一个是5,下下一个是空
//所以head.next.next 就是5->4
head.next.next = head;
head.next = null;//防止链表循环,需要将head.next设置为空
return cur;//每层递归函数都返回cur,也就是最后一个节点
}
146. LRU缓存机制——hashmap
- 自己实现双向链表
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
- LRU一个映射,通过HashMap的key定位Node
方便删除和添加,我们初始化一个伪头结点,和一个伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
//存储映射关系key——>node
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
//最大容量
private int capacity;
//伪头结点,伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
private DLinkedNode head, tail;
- 总体代码
public class No146 {
//自己实现一个双向链表节点
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>();
//存储映射关系key——>node
private int size;
private int capacity;//最大容量
private DLinkedNode head, tail;
//伪头结点,伪尾节点,添加和删除节点的时候不需要判断相邻的链表节点是否存在
public No146(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);//如果key存在,再移到头部
return node.value;
}
public void put(int key,int value){
DLinkedNode node=cache.get(key);
if(node==null){
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{
node.value=value;//如果key存在,先通过哈希表定位,再修改value,并移到头部
moveToHead(node);
}
}
public void addToHead(DLinkedNode node){
DLinkedNode next=head.next;
node.prev=head;
node.next=next;
next.prev=node;
head.next=node;
}
public void removeNode(DLinkedNode node){
node.prev.next=node.next;
node.next.prev=node.prev;
}
public void moveToHead(DLinkedNode node){
removeNode(node);
addToHead(node);
}
public DLinkedNode removeTail(){
DLinkedNode node=tail.prev;
removeNode(node);
return node;
}
}
160. 相交链表——双指针,浪漫相遇
public class No160 {
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 ? pA.next : headB;
pB = pB != null ? pB.next : headA;
}
//若不相交,相遇处是null(都走了一遍a和b)
return pA;
}
}
236. 二叉树的最近公共祖先——递归
class No236{
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//root到了叶子节点,直接返回null
//root=p或者q的时候直接返回
if (root == null || root == p || root == q ){
return root;
}
//向左节点和右节点递归
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left==null){
return right;
}
if(right==null){
return left;
}
return root;
}
}
15.三数之和——三指针
- 排序
- 跳过重复项,减少不必要的判断
- 第一个指针指向的元素大于0,直接break
public class No15 {
/**
* 时间复杂度:O(N^2),外循环kO(N),内循环双指针找和O(N)
* @param nums
* @return
*/
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if (nums == null || nums.length == 0) return res;
Arrays.sort(nums);
//固定第一个指针,剩下两个数用左右指针找
for (int k = 0; k < nums.length - 2; k++) {
//如果第一数(三者里面最小的)都大于0,那么跳过此次循环
if (nums[k] > 0) break;
//跳过重复的,减少不必要的判断
if (k > 0 && nums[k] == nums[k - 1]) continue;
//定义双指针
int i = k + 1;
int j = nums.length - 1;
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if (sum == 0) {
res.add(new ArrayList<>(Arrays.asList(nums[k], nums[i], nums[j])));
//跳过重复的,减少不必要的判断
while (i < j && nums[i] == nums[++i]) ;
while (i < j && nums[j] == nums[--j]) ;
} else if (sum > 0) {
while (i < j && nums[j] == nums[--j]) ;
} else {
while (i < j && nums[i] == nums[++i]) ;
}
}
}
return res;
}
}
121. 买股票的最佳时机——dp(重点)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。注意:你不能在买入股票前卖出股票。
易理解写法
public int maxProfit2(int[] prices) {
int cost = Integer.MAX_VALUE;
int profit = 0;
for (int price : prices) {
cost = Math.min(cost, price);
profit = Math.max(profit, price - cost);
}
return profit;
}
dp模板写法
股票问题其实就是一个三维的dp,哪三维?—— 天数,交易次数,状态
我们用dp[i][k][0 or 1]来表示这个转移状态
dp[i][k][1]:表示第i天还有k次交易次数,手中持有股票的最大利润
dp[i][k][1]:表示第i天还有k次交易次数,手中持没有股票的最大利润
我们就是要求dp[n-1][k][0]
状态转移方程:
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) ##今天我没有持有股票,有两种可能
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) ##今天我持有股票,有两种可能
base case:
dp[-1][k][0] = 0
因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0
dp[-1][k][1] = -infinity
还没开始是不可能持有股票的,⽤负⽆穷表⽰这种不可能
dp[i][0][0] = 0
k 至少是1,k = 0 意味着根本不允许交易,利润当然是 0
dp[i][0][1] = -infinity
不允许交易是不可能持有股票的,⽤负⽆穷表⽰这种不可能
Leetcode121中 k = 1,base case中 dp[i-1][k-1][0] = dp[i-1][0][0] = 0,所以根据状态转移方程 k 只剩下1的情况,可以把k消掉,变成二维dp
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];//[][0]表示没有持有[][1]表示持有
for (int i = 0; i < n; i++) {
if (i - 1 == -1) {//处理base case
dp[i][0] = 0;
//dp[i][0] = Math.max(dp[-1][0], dp[-1][1] + prices[0])
//= Math.max(0,-infinity + price) = 0
dp[i][1] = -prices[i];
//dp[i][0] = Math.max(dp[-1][1], dp[-1][0] - prices[0])
//= Math.max(-infinity,0 - price) = -price
continue;
}
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
}
return dp[n - 1][0];
}
dp优化写法
其实状态转移⽅程中,新状态只和相邻的⼀个状态有关,只需要⼀个变量储存相邻的那个状态就够了,这样可以把空间复杂度降到 O(1)
注意这里的base case和之前的写法不同
public int maxprofit(int[] prices) {
int n = prices.length;
// base case: dp[-1][0] = 0, dp[-1][1] = -infinity
int keep = 0;
int sell = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
keep = Math.max(keep, sell + prices[i]);
sell = Math.max(sell, - prices[i]);
}
return keep;
}
199. 二叉树的右视图——BFS
判断一下遍历每层的queue是否到了最后一个就行了。。。
public class No199 {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (i == size - 1) { //将当前层的最后一个节点放入结果列表
res.add(node.val);
}
}
}
return res;
}
}