一、引言
链表是一种非线性数据结构,各种语言中给链表分配内存时不是一块整体的,可以充分利用碎片内存。相对线性的数据结构,链表对增加和删除节点比较高效,对读节点效率低一些。另外非线性结构需要保存下一个节点的指针,内存占用会大一些。
二、 高频面试题
- 反转链表
/**
* 非递归方式反转链表head 到 end,不包括 end
* @param head
* @param end: 不包括
* @return
*/
public static ListNode reverse1(ListNode head, ListNode end) {
if(head == end || head.getNext() == end) return head;
ListNode pre = null;
ListNode cur = head;
while(cur != end) {
ListNode next = cur.getNext();
cur.setNext(pre);
pre = cur;
cur = next;
}
return pre;
}
/**
* 递归方式反转链表
* @param head
* @return
*/
public static ListNode reverse2(ListNode head) {
if(head == null || head.getNext() == null) return head;
ListNode next = head.getNext();
ListNode newHead = reverse2(next);
next.setNext(head);
head.setNext(null);
return newHead;
}
- K 个一组反转链表
/**
* k个一组反转链表,不足 k 个时不反转
* @param head
* @param k
* @return
*/
public static ListNode reverseKGroup(ListNode head, int k) {
// 加入 dummy 好计算 K 个节点
ListNode dummy = new ListNode(-1);
dummy.setNext(head);
ListNode p = dummy;
for(int i = 0; i < k; i++) {
if(p == null || p.getNext() == null) return head;
p = p.getNext();
}
// 第 2 组的首个节点
ListNode next = p.getNext();
// g1Head为第 1 组反转后的头节点
ListNode g1Head = reverse1(head, next);
// 从第 2 组开始继续递归,g2Head为第 2 组反转后的头节点
ListNode g2Head = reverseKGroup(next, k);
// head 现在为第 1 组的尾节点了
head.setNext(g2Head);
return g1Head;
}
- 求链表中间节点
/**
* 找链表的中间节点。奇数节点返回中间节点;偶数节点返回 2 个中间节点中的第 2 个节点,本场景主要方便回文比较
* 1, 2, 3 返回 2
* 1,2,3,4 返回 3
* @param head
* @return
*/
public static ListNode findTheMid(ListNode head) {
if(head == null) return head;
ListNode slow = head, fast = head;
while(fast != null && fast.getNext() != null) {
slow = slow.getNext();
fast = fast.getNext().getNext();
}
return slow;
}
- 判断链表是否有环
/**
* 通过快慢指针判断链表是否有环
* @param head
* @return
*/
private static boolean hasCycle(ListNode head) {
if(head == null || head.getNext() == null) return false;
ListNode slow = head, fast = head.getNext().getNext();
while(fast != null && fast.getNext() != null) {
if(slow == fast) return true;
slow = slow.getNext();
fast = fast.getNext().getNext();
}
return false;
}
- 判断链表是否是回文
/**
* 判断以 head 开头的链表节点是否构成回文
* @param head
* @return
*/
public static boolean isPalindrome(ListNode head) {
// 找到中间节点。奇数节点返回中间节点;偶数节点返回 2 个中间节点中的第 2 个节点
ListNode mid = findTheMid(head);
// 反转中间节点和尾节点间的元素
ListNode newHead = reverseToTheEnd(mid);
// 顺序比较前后 2 组节点是否一样
while(head != null && newHead != null) {
if(head.getVal() != newHead.getVal()) return false;
head = head.getNext();
newHead = newHead.getNext();
}
return true;
}
- 求 2 个链表的相交节点
/**
* 求 2 个链表的相交节点。如果不存在的话返回 null
* @param head1
* @param head2
* @return
*/
public static ListNode isIntersect(ListNode head1, ListNode head2) {
ListNode p1 = head1;
ListNode p2 = head2;
while(p1 != null && p2 != null) {
if(p1 == p2) return p1;
p1 = p1.getNext();
if(p1 == null) p1 = head2;
p2 = p2.getNext();
if(p2 == null) p2 = head1;
}
return null;
}
三、总结
- 为了把对头节点和其他节点的处理统一,一般会加入dummy 节点。这样边界逻辑处理会少一些;
- 链表算法中关键点是前后节点指针的处理,需要避免构成循环;
- 多练、多推演。