141.环形链表
该题目是判断该链表是否存在环形结构,我们可以使用额外O(N)空间对结点进行保存,看是否存在重复。
也可以使用我们常用的快慢结点法,判断快慢结点是否会碰头。
//判断是否为环形链表
public boolean hasCycle(ListNode head) {
//快慢指针法
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next;
fast = fast.next;
if(fast == slow) {
return true;
}
}
return false;
}
142.环形链表2
相比141,该题目是返回环的入口,这里也可以采用双指针法进行解决(可以看LC题解),我这里使用了额外空间进行计算。
//找到环入口
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
while(head != null) {
if(set.contains(head)) {
return head;
}else {
set.add(head);
head = head.next;
}
}
return null;
}
143.重排链表
该题目有点类似于链表知识的汇总。
使用到了链表中点,链表反转,链表拆分和合并
需要注意的一点就是在奇数结点情况时,前半部分结点会在新链表的尾部,此时它的next仍旧指向的是后半段部分,所以我们需要对其进行断开,不然会导致出现问题。
//链表重排(首尾相连)
public void reorderList(ListNode head) {
//获得中位节点(以中位节点为首,进行后半边的链表反转)
ListNode mid = getMid(head);
if(mid == null) {
return;
}
//进行反转
ListNode reserve = reserve(mid);
while(reserve != null) {
//记录reserve的下一个结点
ListNode next = reserve.next;
reserve.next = head.next;
head.next = reserve;
head = reserve.next;
reserve = next;
}
//如果是奇数,则需要在最后一个结点的next进行断开(之前仍旧与后半段反转链表相连)
if(head != null) {
head.next = null;
}
}
//获得中点
public ListNode getMid(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next;
fast = fast.next;
}
return slow;
}
//反转链表
public ListNode reserve(ListNode node) {
if(node == null) {
return null;
}
//原地反转
ListNode ans = new ListNode();
ans.next = node;
ListNode tail = node;
//说明还有需要反转的
while(tail.next != null) {
ListNode next = tail.next;
tail.next = next.next;
//更换头部
next.next = ans.next;
//更换假头结点指向
ans.next = next;
}
return ans.next;
}
144.二叉树的前序遍历
递归实现大家肯定都会了,我们就尝试使用迭代法完成。
主要是通过使用栈完成的,先序遍历是 根-左-右
那么我们可以直接就先加入根结点的值,再放入右左结点(这里注意顺序),等下一轮弹出栈顶进行判断是否存在右左结点,存在继续放入。
//前序遍历(迭代法)
public List<Integer> preorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
List<Integer> list = new ArrayList<>();
if(root == null) {
return list;
}
stack.push(root);
while(!stack.isEmpty()) {
TreeNode pop = stack.pop();
list.add(pop.val);
//因为这里用栈存储,记得先右后左(保证右节点最晚访问)
if(pop.right != null) {
stack.push(pop.right);
}
if(pop.left != null) {
stack.push(pop.left);
}
}
return list;
}
145.二叉树的后序遍历
该题目也采用迭代法。
后序遍历(左-右-根),那我们在方式的时候,就把根结点放到队首,然后按左右结点入栈。
注意:这里是左右,先序遍历是右左。
这里又变成左右的原因是:
首先我们存放结点的数据结构是一个双向链表,然后我们通过以头插法方式将数据存入链表,头插法有点类似于栈的先进后出,然后我们存放树的结点也是使用栈,所以这两个栈的概念最后就变成了顺序的样子(可以尝试在脑袋里跑一下)
//后序遍历(左-右-根)
public List<Integer> postorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
LinkedList<Integer> list = new LinkedList<>();
if(root == null) {
return list;
}
stack.push(root);
while(!stack.isEmpty()) {
TreeNode pop = stack.pop();
list.addFirst(pop.val);
if(pop.left != null) {
stack.push(pop.left);
}
if(pop.right != null) {
stack.push(pop.right);
}
}
return list;
}
LRU缓存机制
LRU是通过内部维护了一个存活链表,即最近使用过的数据(key,value)对应的key会在链表的头部(即按照活跃度排序的链表),那么此时的链尾元素就是最不活跃的链表。
这里需要考虑的就是,更新问题和缺页问题的情况。
更新问题是不需要进行页面替换,只需要更新缓存和活跃链表即可。
缺页问题则需要进行页面替换,即淘汰队尾元素,清空队尾缓存,然后加入新的缓存。
class LRUCache {
private int capacity;
private int size = 0;
//LRU维护的双向链表
private Deque<Integer> deque = new LinkedList<>();
//保存LRU缓存
private Map<Integer,Integer> map = new HashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
//说明存在该值
if(map.containsKey(key)) {
deque.remove(key);
//将其在活跃链表的位置进行更新
deque.addFirst(key);
return map.get(key);
}else {
return -1;
}
}
public void put(int key, int value) {
//说明是更新,不属于缺页部分
if(map.containsKey(key)) {
map.put(key,value);
deque.remove(key);
//将其活跃度提升到最前面
deque.addFirst(key);
}else {
//说明添加的是新值,并且发生缺页,需要进行缺页更替
if(size == capacity) {
//找出最近最久未使用的
Integer last = deque.pollLast();
map.remove(last);
size--;
}
map.put(key,value);
deque.addFirst(key);
size++;
}
}
}
147.对链表进行插入排序
链表的插入排序和数组插入排序都是需要从元素的第二个开始考虑插入的位置,但是链表和数据有个区别在于:数组元素之间无关联,链表元素之间可能存在next联系
所以在插入元素的前,我们需要先将他们的联系断开,否则可能会导致死循环,即把next = null。
然后就根据插入排序的思想, 在已排好序的部分需要插入点,我们需要连个指针把元素的位置锁定住。即多一个prev前驱结点。
//链表的插入排序
public ListNode insertionSortList(ListNode head) {
if(head == null) {
return null;
}
//傀儡头结点
ListNode ans = new ListNode();
ans.next = head;
//从链表的下一位开始
ListNode start = head.next;
//断开
head.next = null;
//从start开始进行插入排序
while(start != null) {
//start指向可能会发生改变,所以需要提前保存
ListNode next = start.next;
//断开
start.next = null;
//从[head,start)搜索合适的插入点
ListNode prev = ans;
ListNode temp = ans.next;
while(temp != null && temp.val < start.val) {
//指针移动
prev = temp;
temp = temp.next;
}
//此时说明找到了合适的位置
prev.next = start;
start.next = temp;
start = next;
}
return ans.next;
}
148.排序链表
我们也可以通过保存链表结点的方法来对其进行排序,也可以使用归并排序。
归并排序就是将链表拆成两个部分,在对两个部分进行排序,拍完序后就是两个有序链表的合并。
这个题目和上面链表插入排序需要注意的是一样的,即链表next的断开。
将他们的链表结点之间的关系完全断开,才能进行合并。
//链表排序(归并)
public ListNode sortList(ListNode head) {
return sortList(head,null);
}
public ListNode sortList(ListNode head, ListNode tail) {
if(head == null) {
return null;
}
//说明此时只有一个节点,直接断开即可(tail为不可达结点)
//在这一步完成了将每个节点断开
if(head.next == tail) {
head.next = null;
return head;
}
//分成两个链表
ListNode mid = getMid(head,tail);
//断裂成两个完全独立的链表
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid,tail);
return merge(list1,list2);
}
//获取中点
public ListNode getMid(ListNode head, ListNode tail) {
ListNode slow = head;
ListNode fast = head;
while(fast != tail && fast.next != tail) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
//合并
public ListNode merge(ListNode list1, ListNode list2) {
ListNode ans = new ListNode();
ListNode head = ans;
while(list1 != null && list2 != null) {
if(list1.val > list2.val) {
head.next = list2;
list2 = list2.next;
}else {
head.next = list1;
list1 = list1.next;
}
head = head.next;
}
while(list1 != null) {
head.next = list1;
list1 = list1.next;
head = head.next;
}
while(list2 != null) {
head.next = list2;
list2 = list2.next;
head = head.next;
}
return ans.next;
}
150.逆波兰表达式
了解这个概念就行了,这里我是使用了一个栈用于保存计算结果,逆波兰表达式就是碰到了运算符就从栈弹出两个元素进行计算,计算结果在入栈。
**注意:**针对于 ‘/’ 和 ‘-’ 需要注意这个弹出元素的顺序。
//逆波兰表达式
public int evalRPN(String[] tokens) {
Stack<Integer> number = new Stack<>();
for(String token : tokens) {
if(token.equals("+")) {
number.push(number.pop() + number.pop());
}else if(token.equals("-")) {
Integer num1 = number.pop();
Integer num2 = number.pop();
number.push(num2 - num1);
}else if(token.equals("*")) {
number.push(number.pop() * number.pop());
}else if(token.equals("/")) {
Integer num1 = number.pop();
Integer num2 = number.pop();
number.push(num2 / num1);
}else {
number.push(Integer.parseInt(token));
}
}
return number.pop();
}