LeetCodeHot100_0x05
LeetCodeHot100_0x05
32. 随机链表的复制(不熟)
求解思路: 这题还是由于对链表操作不熟,没能理解题目什么意思,再看了官方解释后才决定用哈希表解决该问题。具体思路:先用哈希存好数据,再分布创建val 和 next 、random。
【哈希法】
class Solution {
public Node copyRandomList(Node head) {
// 这题的难点在于如果按照常规遍历创建新节点,当前节点的random节点可能还没创建
// 解决策略:分步创建,先创建好val和next,后面再补充random
// 解决方法:哈希表存储当前节点和新节点键值对
if(head == null) return null;
Node curr = head;
Map<Node,Node> hm =new HashMap<>();
// 第一遍遍历:创建映射关系
while(curr != null) {
hm.put(curr,new Node(curr.val));
curr = curr.next;
}
// 第二遍遍历,根据映射关系补充next和random
curr = head;
while(curr != null) {
hm.get(curr).next = hm.get(curr.next); // 新节点的next节点为映射键节点的next
hm.get(curr).random = hm.get(curr.random); // 同理
curr = curr.next;
}
return hm.get(head); // 由于是new Node() 创建出来的,所以必然与原head地址值不同
}
}
33. 排序链表
求解思路: 朴素做法是把值取出来排序再塞回去。进阶做法:限制O(NlogN)的排序无非两种,归并排序和快速排序。其中归并排序是稳定的排序,而且体现了分而治之的思想,所以我们尝试写个链表版的归并排序来解决这个问题。
【排值】把值取出来进行排序再塞回去
class Solution {
public ListNode sortList(ListNode head) {
// 取出来
List<Integer> value = new ArrayList<>();
ListNode curr = head;
while(curr != null) {
value.add(curr.val);
curr = curr.next;
}
// 排序
Collections.sort(value);
// 填回
curr = head;
int i = 0;
while(curr != null) {
curr.val = value.get(i);
i++;
curr = curr.next;
}
return head;
}
}
【归并排序】
class Solution {
public ListNode sortList(ListNode head) {
return mergeSort(head);
}
// 归并排序,链表版
// 思想: 先分治,后合并
private ListNode mergeSort(ListNode head) {
// 没有节点或只有一个节点无需排序,直接返回
if(head == null || head.next == null) return head;
//1. 利用快慢指针找到中间点作为基准
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow.next;
// 注意!这一步特别重要,断开链表!,这样左半部分就是head到slow了
slow.next = null;
//2. 对左半部分进行归并排序
ListNode l = mergeSort(head);
//3. 对右半部分进行归并排序
ListNode r = mergeSort(mid);
//4. 将左右链表进行合并
return mergeList(l,r);
}
private ListNode mergeList(ListNode l,ListNode r) {
ListNode newHead = new ListNode(-1);
ListNode curr = newHead;
while(l != null && r != null) {
if(l.val < r.val) {
curr.next = l;
l = l.next;
}else {
curr.next = r;
r = r.next;
}
curr = curr.next;
}
curr.next = (l != null) ? l : r;
return newHead.next;
}
}
34. 合并k个升序链表
求解思路:
【改值不改结构法】
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 朴素做法,改值不改结构
if(lists.length == 0) return null;
List<Integer> values = new ArrayList<>();
for(int i = 0;i< lists.length;i++) {
ListNode curr = lists[i];
while(curr != null) {
values.add(curr.val);
curr = curr.next;
}
}
Collections.sort(values);
ListNode NewHead = new ListNode(-1);
ListNode curr = NewHead;
for(int i=0;i<values.size();i++) {
curr.next = new ListNode(values.get(i));
curr = curr.next;
}
return NewHead.next;
}
}
【n指针遍历法】就是合并两个有序链表的升级版,给n条链表各一个指针进行遍历
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 指针做法,每个链表都有一个维护指针
int n = lists.length;
ListNode NewHead = new ListNode(-1);
ListNode curr = NewHead;
while(true) {
ListNode curMinNode = null; // 用于保存当轮最小值的节点信息
int minidx = -1; // 用来保存当前最小值位于哪条链表,同时用于判断遍历是否完成
for(int i=0;i<n;i++) {
// 判断当前链表是否已经遍历完
if(lists[i] == null) continue;
// 寻找当前位置的最小值
if(curMinNode == null || curMinNode.val > lists[i].val) {
curMinNode = lists[i];
minidx = i;
}
}
// 这里表明全部遍历完了
if(minidx == -1) {
break;
}
// 指针移动
curr.next = curMinNode;
curr = curr.next;
lists[minidx] = lists[minidx].next;
}
return NewHead.next;
}
}
【迭代法】也是合并两个有序链表的升级版,只需要逐条逐条的迭代合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) return null;
int n = lists.length;
ListNode res = lists[0];
for(int i=1;i<n;i++) {
res = merge2List(res,lists[i]);
}
return res;
}
private ListNode merge2List(ListNode l1, ListNode l2) {
// 创建一个哑节点,以便于处理头节点
ListNode dummy = new ListNode(0);
ListNode current = dummy;
// 合并两个有序链表
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
// 连接剩余节点
if (l1 != null) {
current.next = l1;
} else if (l2 != null) {
current.next = l2;
}
// 返回合并后的链表(跳过哑节点)
return dummy.next;
}
}
35. LRU缓存(重点手撕面经题)
说明: 这一题是很重要的面试题,需要实现一个最近最少使用缓存结构,
难点在于思考底层数据结构的选择。由于get、put函数的要求是O(1),且要求能O(1)删除最远古的元素。这提示我们设计数据结构时考虑哈希表 和 双向链表。我一开始想着用Map+LinkedList的结构去实现,发现LinkedList的get方法和remove方法底层都是要遍历链表的(因为虽然LinkedList底层也是双向链表结构,但是是基于索引的,每个节点之间是无法直接找到的,需要遍历,不符合)。
难点2在于数据结构的设计,题解在设计双向链表时,引入了虚拟头和虚拟尾,减少了很多越界特判的情况发生,从而简化了代码书写,思维特别巧妙。
最后,对于这一题来说,多理解,多写,多背。如果面试真的遇到了,希望你不仅能写的出来,还能阐述实现的思路。
class LRUCache {
class DLinkedNode {
int key; // 键
int value; // 值
DLinkedNode prev; // 存储前驱节点
DLinkedNode next; // 存储后继节点
public DLinkedNode() {} // 无参构造
public DLinkedNode(int key, int value) { // 有参构造
this.key = key;
this.value = value;
}
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size; // 当前大小
private int capacity; // 需要的初始容量
private DLinkedNode head,tail; // 虚拟头节点和虚拟尾节点
/**
* 初始化函数
*/
public LRUCache(int capacity) {
// 以正整数作为容量capacity初始化LRU缓存
this.capacity = capacity;
this.size = 0;
// 创建虚拟头尾节点,构造双向链表闭环
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
/**
* 获取值函数
*/
public int get(int key) {
// 如果key存在于缓存中,返回关键字的值,否则返回-1
// O(1)
DLinkedNode node = cache.get(key); // 利用哈希表快速判断有没有该节点
if(node == null) return -1; // 没有返回-1
// 如果存在,需要将它移动到头部(先删后加)
moveToHead(node);
return node.value;
}
/**
* 添加值函数
*/
public void put(int key, int value) {
// 如果key已存在,变更为value
// 如果不存在,插入key-value
// 如果key数量超过capacity,逐出最久未使用关键字
// O(1)
DLinkedNode node = cache.get(key);
if(node != null) { // 存在的情况,替换 + 移动头部
node.value = value;
moveToHead(node);
}else { // 不存在的情况 判断容量 + 触发删除 + 头部添加
// 创建元素
DLinkedNode newNode = new DLinkedNode(key,value);
// 添加哈希表
cache.put(key,newNode);
// 头部添加
addToHead(newNode);
// 判断容量
++size;
if(size > capacity) {
// 触发删除尾部元素
DLinkedNode tail = removeTail();
// 将尾部元素移除哈希表
cache.remove(tail.key);
--size;
}
}
}
private void addToHead(DLinkedNode node) {
node.prev = head; // 前驱节点是虚拟头节点
node.next = head.next; // 后继节点是原虚拟头节点的后继节点
head.next.prev = node; // 原虚拟头节点的后继节点的前驱节点变为node
head.next = node; // 虚拟头节点的后继节点改为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 targetNode = tail.prev; // 要移除的节点在虚拟尾节点的前面
removeNode(targetNode); // 删除目标节点
return targetNode; // 返回
}
}
36. 二叉树的中序遍历
求解思路: 涉及到二叉树的题目,基本上都是递归和迭代两种解法。递归类似深度搜索,一条路走到黑,而迭代法像是一层一层去做,通常会使用队列数据结构。
【递归法】
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
LDR(root,res); // 从根开始中序遍历
return res;
}
private void LDR(TreeNode root,List<Integer> res) {
if(root == null) return;
LDR(root.left,res); // 左
res.add(root.val); // 中
LDR(root.right,res); // 右
}
}
【迭代法】
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// 迭代写法,借助栈结构
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty()) {
// 迭代处理左子树(一直深入到子节点)
while(root!=null) {
stack.push(root);
root = root.left;
}
root = stack.pop();// 弹出左子树节点并检查当前节点有没有右子树,有的话继续重复操作
res.add(root.val);
root = root.right;
}
return res;
}
}
37. 二叉树的最大深度
【递归法】
class Solution {
int res = 0;
public int maxDepth(TreeNode root) {
if(root == null) return 0;
res = Math.max(maxDepth(root.left),maxDepth(root.right)) + 1;
return res;
}
}
38. 翻转二叉树
【递归法】
拿这张图理解,假设1,3节点就是简单节点,那就直接交换1,3就行了。但是现实是1,3节点可能也是一个二叉树。那就递归处理,先把1,3节点的子树给翻转完毕,最后轮到1,3节点就行了。
在实现上,只需要在交换1,3节点的时候,套上一个invertTree() 函数就可以实现递归处理。
class Solution {
public TreeNode invertTree(TreeNode root) {
// 整体法,将二叉树退化成两层最简单的二叉树,那么我们要做的就是
// 左叶子节点与右叶子节点交换
// 然后扩展到普通多层二层树:
// 只要当前节点不是最简二层二叉树,那就用invertTree把它底下的结构先改变了
// 这里右用到了递归的思想
if(root == null) return null;
TreeNode left = root.left;
root.left = invertTree(root.right);
root.right = invertTree(left);
return root;
}
}
39. 总结
0x05遇到的题目主要是链表和树结构。通过前面两天刷链表题的经验,0x05遇到的题目即使有些不熟也是能写出来的。树方面的题目暂时也没有遇到比较难的题目,全部都是考察递归和对树形结构的基本了解吧。总结以下每一题:
- 随机链表的复制
哈希
- 由于随机链表的结构特性,这题先创建好普通链表的复制,最后将random指针填回去就行
- 排序链表
归并排序
- 朴素做法对值进行快排,进阶做法锻炼归并排序的链表版
- 合并K个升序链表
n指针、递归
- 这题是合并两个升序列表的进阶,所以解法也一样。n指针维护、递归逐条合并等
- LRU缓存
- 这题根据题目的意思,需要我们自己实现一个 哈希 + 双向链表的数据结构
- 二叉树中序遍历
递归
- 递归 左 值 右即可
- 二叉树最大深度
递归
- 递归 (左,右)Max + 1 即可
- 二叉树翻转
递归
- 递归 (左 ,右)Swap 即可