发现面试链表问的真的多。有时候面试官只让说思路,所以拿笔画一画,要能把思路捋清。
链表数据结构
class ListNode{
int val;//节点有个值
ListNode next;//节点有下一个节点
ListNode(int val){//构造函数
this.val = val;
}
}
反转链表
思路:首先定义一个前驱节点为空,然后用一个当前指针cur指向当前节点,循环的调换方向。循环体四小步:(1)临时变量next保存当前节点cur的下一个节点;(2)当前节点cur的下一个节点指向前驱节点pre;(3)当前节点cur赋给前驱节点pre;(4)临时变量next赋给当前节点cur。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;//临时变量next保存当前节点cur的下一个节点
cur.next = pre;//当前节点cur的下一个节点指向前驱节点pre
pre = cur;//当前节点cur赋给前驱节点pre
cur = next;//临时变量next赋给当前节点cur
}
return pre;
}
}
LRU实现
NC93 设计LRU缓存结构
146. LRU 缓存机制
有点难!但是最近面试官好像都喜欢考。
思路:这个问题用双向链表辅助哈希表来实现。
- 首先自己得能写出来一个双向链表的结构。
- 分别定义头尾节点,并互相连接
- 遍历提供的操作列表,先判断是什么操作
- 若插入,先看看哈希表里有键值冲突:没有的话,用键值新构造一个节点,插入到链表中,并移到链表头,还要size++,再看看有没有越界,越了的话要把尾部节点删除;同时放入哈希表中。有的话,用新值代替旧值,然后移到链表头。
- 若读取,判断键对应的节点存不存在,存在的话读值并把节点移到头部,不存在-1.
public class Solution {
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
class DoubleListNode {
int key;
int val;
DoubleListNode pre;
DoubleListNode next;
DoubleListNode() {}
DoubleListNode(int key, int val) {
this.key = key;
this.val = val;
}
}
DoubleListNode head = new DoubleListNode(-1, -1);
DoubleListNode tail = new DoubleListNode(-1, -1);
HashMap<Integer, DoubleListNode> map = new HashMap<>();
public int[] LRU (int[][] operators, int k) {
// write code here
ArrayList<Integer> ans = new ArrayList<>();
head.next = tail;
tail.pre = head;
int size = 0;
for(int i = 0; i < operators.length; i++){
int tmp_key = operators[i][1];
DoubleListNode node = map.get(tmp_key);
if(operators[i][0] == 1){
if(node == null){
DoubleListNode new_node = new DoubleListNode(tmp_key, operators[i][2]);
map.put(tmp_key, new_node);
add2head(new_node);
size++;
if(size > k){
DoubleListNode weiba = removeTail();
map.remove(weiba.key);
size--;
}
}else{
node.val = operators[i][2];
add2head(node);
}
}else{
// node = map.get(tmp_key);
if(node == null) ans.add(-1);
else{
ans.add(node.val);
move2head(node);
}
}
}
int[] res = new int[ans.size()];
for(int i = 0; i < ans.size(); i++){
res[i] = ans.get(i);
}
return res;
}
void move2head(DoubleListNode node){
removeNode(node);
add2head(node);
}
void removeNode(DoubleListNode node){
node.pre.next = node.next;
node.next.pre = node.pre;
}
void add2head(DoubleListNode node){
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}
DoubleListNode removeTail(){
DoubleListNode weiba = tail.pre;
removeNode(tail.pre);
return weiba;
}
}
判断链表有环
141. 判断是否有环
思路:快慢指针,只要快指针和快指针的next均不为空,就循环的让两个指针后移。一旦发现两个指针相等了,说明有环。否则循环结束必然是遇到了空,没环。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow) return true;
}
return false;
}
}
142. 环形链表 II-找到入环节点
思路一:直接用hashset不重复的特点,遍历链表,什么时候add失败了或者contains方法true了,说明遇到了。
public class Solution {
public ListNode detectCycle(ListNode head) {
HashSet<ListNode> set = new HashSet<>();
ListNode cur = head;
while(cur != null){
if(set.contains(cur)){// or "!set.add(cur)"
return cur;
}
set.add(cur);
cur = cur.next;
}
return null;
}
}
思路2:快慢指针,仔细听,首先fast和slow分别从head出发,fast跨两步,slow跨一步,等什么时候相遇了,fast走过了slow两倍的距离。
也就是图中x = z。那么这个时候fast回到head,slow在原位,他们同时一步一步走,正好在入口相遇。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}
找出两个链表的交点
160. 相交链表
思路:两条链表长度不一致,但是如果两个指针,pA先走A链表,再走B链表;pB先走B链表,再走A链表,那么等他们什么时候相遇了,便是链表相交的地方。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA;
ListNode pB = headB;
while(pA != pB){
if(pA == null) pA = headB;
else pA = pA.next;
if(pB == null) pB = headA;
else pB = pB.next;
}
return pA;
}
}
删除链表的倒数第 N 个结点
找倒数那道题就pass了。
19. 删除链表的倒数第 N 个结点
思路还是快慢指针了,快指针先往后走n步,然后快慢指针同时往后走,等fast走到头了,slow也就到我们要找的那个节点了。因为要删除,还要另外用个指针指向前驱结点。为了应对删除头节点的情况,前边加个哑结点辅助返回。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(-1);
ListNode preNode = pre;
pre.next = head;
ListNode fast = head;
ListNode slow = head;
for(int i = 0; i < n; i++){
fast = fast.next;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
pre = pre.next;
}
pre.next = slow.next;
return preNode.next;
}
}
K 个一组翻转链表
25. K 个一组翻转链表
思路:这个有点难。
- 定一个哑结点指向链表,辅助返回
- 循环的找k个节点构成一组,如果凑不足k个了,就返回
- 凑足了,定一个临时节点保存下一组的头结点(当前组的尾节点的next)
- 翻转当前组,翻转的时候尾巴会成功指向下一组的头
- 翻转操作类似反转链表,但要传入头尾节点,返回翻转后的头尾结点
- 翻转完了 前驱结点指向当前新的组
- 更新pre和head
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0);
ListNode pre = hair;
hair.next = head;
while(head != null){
ListNode tail = pre;
for(int i = 0; i < k; i++){
tail = tail.next;
if(tail == null){
return hair.next;
}
}//此时 head指向第k组的头部节点,tail指向尾部节点
ListNode nex = tail.next;
ListNode[] reverse = reverseAGroup(head, tail);
head = reverse[0];
tail = reverse[1];
pre.next = head;
//tail.next = nex;
pre = tail;
head = tail.next;
}
return hair.next;
}
public ListNode[] reverseAGroup(ListNode head, ListNode tail){
ListNode pre = tail.next;
ListNode cur = head;
while(tail != pre){
ListNode nex = cur.next;
cur.next = pre;
pre = cur;
cur = nex;
}
return new ListNode[]{tail, head};
}
}
排序链表
148.排序链表
对链表进行排序,要求时间复杂度O(nlogn),空间复杂度O(1)。
使用归并排序。建议对比着数组的归并排序一起看。自顶向下的归并空间复杂度为O(n),而自底向上的归并可以做到O(n)。不过可以看出链表的归并只分治了一次。
class Solution {//自顶向上
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail) {
if (head == null) {//判断链表为空
return head;
}
if (head.next == tail) {//只有一个节点
head.next = null;
return head;
}
ListNode slow = head, fast = head;//快慢指针寻找中间节点
while (fast != tail) {
slow = slow.next;
fast = fast.next;
if (fast != tail) {
fast = fast.next;
}
}
ListNode mid = slow;
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
ListNode sorted = merge(list1, list2);
return sorted;
}
//线性合并
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}
自底向上的递归方法就是从左往右,每次对 len 个节点进行线性合并,下一轮再对 len*2 个节点线性合并。力扣讲解的很到位:
class Solution {//自底向上
public ListNode sortList(ListNode head) {
if (head == null) {
return head;
}
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
ListNode dummyHead = new ListNode(0, head);
for (int subLength = 1; subLength < length; subLength <<= 1) {
ListNode prev = dummyHead, curr = dummyHead.next;
while (curr != null) {
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
curr = curr.next;
}
ListNode head2 = curr.next;
curr.next = null;
curr = head2;
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
curr = curr.next;
}
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null;
}
ListNode merged = merge(head1, head2);
prev.next = merged;
while (prev.next != null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}