概述
链表作为面试必问的数据结构之一,掌握关于链表的基础算法很有必要。
链表的定义:
public class ListNode {
private ListNode next;
private int val;
ListNode(int val) {
this.val = val;
}
}
Java语言里,链表ListNode并不是一个简单的结构,为啥可以使用==
来判断?
在Java中,==
运算符用于比较两个引用是否指向同一个对象,检查的是引用本身,而不是引用指向的对象的内容。ListNode通常是通过引用来进行操作。使用==
运算符判断两个链表节点是否相等,是为了判断它们是否指向同一个内存地址(即它们是否是同一个对象),而不是判断它们的值是否相等。
二分查找
链表能不能使用二分查找?
不能。二分查找是一种高效的查找算法,适用于已排序且静态的数组或列表,而链表不支持随机内存访问,即每一个节点的地址不能在O(1)的时间复杂度内获得。
判断单向链表是否有环
经典题,解决思路:
- 取数链表的元素值,存入到额外的数据结构,如HashSet。从head节点出发,如果节点的值不在HashSet里面,则存入;继续移动节点,如果节点数值存在于HashSet里,则表明有环。
- 设置两个指针,都从链表头节点出发,一个每次向后移动一步,另一个移动两步,速度不一样,如果存在环,则一定会相遇:
/**
* 判断给定链表是否有环
*/
public static boolean hasLoop(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
while (true) {
if (fast == null || fast.next == null) {
// fast走到链表尾
return false;
} else if (fast.next == slow || fast == slow) {
return true;
} else {
slow = slow.next;
fast = fast.next.next;
}
}
}
引申
- 找到环的入口节点
fast每次走两步,slow每次走一步,slow和fast能重合,则确定单向链表有环路。接下来,让fast回到链表头部,每次步长为1,则当slow和fast再次相遇时,就是环路的入口。
输入两个链表,找出第一个公共结点
- 链表是否为空
- 链表是否是无环链表?是否是单(向)链表?
- 如果两个链表存在公共结点,那从公共结点开始一直到链表的结尾都是一样的,因此只需要从链表的结尾开始,往前搜索,找到最后一个相同的结点即可。单向链表,只能从前向后搜索,借助栈来完成。先把两个链表依次装到两个栈中,然后比较两个栈的栈顶结点是否相同,如果相同则出栈,如果不同,那最后相同的结点就是公共节点。
- 先求2个链表的长度,让长的先走两个链表的长度差,然后再一起走,直到找到第一个公共结点。
- 由于2个链表都没有环,可以把第二个链表接在第一个链表后面,这样就把问题转化为求环的入口节点问题。
- 两个指针p1和p2分别指向链表A和链表B,它们同时向前走,当走到尾节点时,转向另一个链表,比如p1走到链表A的尾节点时,下一步就走到链表B,p2走到链表B的尾节点时,下一步就走到链表A,当
p1==p
2时,就是链表的相交点
/**
* 两个链表的第一个公共结点
*/
public static ListNode findFirstCommonNode(ListNode current1, ListNode current2) {
HashMap<ListNode, Integer> hashMap = new HashMap<>(16);
while (current1 != null) {
hashMap.put(current1, null);
current1 = current1.next;
}
while (current2 != null) {
if (hashMap.containsKey(current2)) {
return current2;
} else if (getNoLoopLength(current2) == 1) {
// 链表2长度为1的特殊情况
return null;
}
current2 = current2.next;
}
return null;
}
/**
* 无环单链表长度
*/
private static int getNoLoopLength(ListNode head) {
int length = 0;
ListNode current = head;
while (current != null) {
length++;
current = current.next;
}
return length;
}
链表翻转
/**
* 链表翻转
*/
public static ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next; // 临时保存下一个节点
cur.next = prev; // 反转当前节点的指针
prev = cur; // 移动prev指针
cur= next; // 移动cur指针
}
return prev; // prev最终指向反转后的新头结点
}
链表旋转
给定链表头节点head,旋转链表,将链表每个节点向右移动k个位置。
/**
* 给定链表头节点head,旋转链表,将链表每个节点向右移动k个位置
*/
public static ListNode rotateRight(ListNode head, int k) {
if (head == null) {
return null;
}
int len = 0;
ListNode tmp = head;
while (tmp != null) {
tmp = tmp.next;
len++;
}
// 判断链表长度和k的关系
k = k % len;
ListNode fast = head;
ListNode slow = head;
// 两指针相差k(修正后)节点
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// fast指向最后一个节点,和原链表的头节点连接起来
fast.next = head;
// slow指向节点的下一个节点是倒数第k个节点,即旋转后的头节点
ListNode newHead = slow.next;
// 断开链接避免成环
slow.next = null;
return newHead;
}
求链表倒数第k个节点
给定一个单向链表,输出该链表中倒数第k个节点,链表的倒数第0个节点为链表的尾指针。
分析:设置两个指针fast、slow都指向head,fast向前走k步,这样fast和slow之间就间隔k个节点,fast和slow同时向前移动,直至slow走到链表末尾。
/**
* 求链表倒数第k个节点
*/
public static ListNode getKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
ListNode slow = head;
// 让快指针先移动k步,如果链表长度小于k,返回null
for (int i = 0; i < k; i++) {
if (fast == null) {
return null;
}
fast = fast.next;
}
// 快慢指针一起移动,直到快指针到达链表末尾
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 返回慢指针所指的节点
return slow;
}
求链表中间节点
求链表的中间节点,如果链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。
分析:可以先求链表的长度,然后计算出中间节点所在链表顺序的位置。但如果只能扫描一遍链表?最高效的解法,通过两个指针来完成。用两个指针从链表头节点开始,一个指针每次向后移动两步,一个每次移动一步,直到快指针移到到尾节点,那么慢指针即是所求。
/**
* 求链表中间节点
*/
public static ListNode middleNode(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
// 快指针每次移动两步,慢指针每次移动一步
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 返回慢指针所指的节点,即为链表的中间节点
return slow;
}
删除链表元素
给定一个链表头节点head和整数val ,删除链表中所有满足Node.val == val
的节点,并返回新的头节点。
public static ListNode removeElements(ListNode head, int val) {
ListNode top = new ListNode(0);
top.next = head;
ListNode pre = top;
ListNode temp = head;
while (temp != null) {
if (temp.val == val) {
pre.next = temp.next;
} else {
pre = temp;
}
temp = temp.next;
}
return top.next;
}
合并两个有序链表
初始化一个空的链表,遍历两个链表直到其中一个为空,然后将剩余的节点接到新链表的末尾。
/**
* 合并两个有序链表并保持输出链表有序
*/
public static ListNode merge(ListNode l1, ListNode l2) {
ListNode tmp = new ListNode();
ListNode cur = tmp;
// 遍历两个链表,直到其中一个链表为空
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur= cur.next;
}
// 将剩余节点接到新链表末尾
if (l1 != null) {
cur.next = l1;
} else {
cur.next = l2;
}
// 返回新链表头节点
return tmp.next;
}
升序排序
可使用归并排序算法,合并方法参考上面。
/**
* 归并排序实现链表的排序:将链表递归地拆分成两个子链表,分别对其排序,然后将排序后的子链表合并成一个有序链表
*/
public static ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 使用快慢指针找到链表的中间点
ListNode slow = head;
ListNode fast = head;
ListNode prev = null;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
// 断开链表
prev.next = null;
// 递归排序左右部分
ListNode l1 = sortList(head);
ListNode l2 = sortList(slow);
// 合并两个有序链表
return merge(l1, l2);
}
删除排序链表中重复元素
/**
* 删除排序链表中重复元素
*/
public static ListNode deleteDuplicates1(ListNode head) {
if (head == null) {
return null;
}
ListNode current = head;
// 遍历链表
while (current.next != null) {
// 如果当前节点的值等于下一个节点的值
if (current.val == current.next.val) {
// 跳过下一个节点
current.next = current.next.next;
} else {
// 移动到下一个节点
current = current.next;
}
}
return head;
}
K个一组翻转链表
给定一个链表,每k个节点一组进行翻转,返回翻转后的链表。k为小于或等于链表长度的正整数。如果节点总数不是k的整数倍,最后剩余的节点保持原有顺序。
public static ListNode reverseKGroup(ListNode head, int k) {
if (head == null) {
return null;
}
ListNode b = head;
for (int i = 0; i < k; i++) {
if (b == null) {
return head;
}
b = b.next;
}
ListNode newHead = reverse(head, b);
head.next = reverseKGroup(b, k);
return newHead;
}
private static ListNode reverse(ListNode a, ListNode b) {
ListNode pre, cur, nxt;
pre = null;
cur = a;
nxt = a;
while (nxt != b) {
nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
分隔链表
给定一个链表头节点head和一个特定值val,把所有小于val的节点都放置在大于或等于val的节点之前,保留两个分区中每个节点的初始相对位置。
public static ListNode partition(ListNode head, int x) {
ListNode tmpHead1 = new ListNode(0);
ListNode tmpHead2 = new ListNode(0);
ListNode node1 = tmpHead1;
ListNode node2 = tmpHead2;
while (head != null) {
if (head.val < x) {
node1.next = head;
head = head.next;
node1 = node1.next;
node1.next = null;
} else {
node2.next = head;
head = head.next;
node2 = node2.next;
node2.next = null;
}
}
node1.next = tmpHead2.next;
return tmpHead1.next;
}
有序链表转换二叉搜索树
给定一个升序单链表,将其转换为高度平衡的二叉搜索树,即二叉树每个节点的左右两个子树的高度差的绝对值不超过1。注意:满足条件的二叉搜索树不止一个。
public static TreeNode sortedListToBST(ListNode head) {
if (head == null) {
return null;
}
return helper(head, null);
}
private static TreeNode helper(ListNode start, ListNode end) {
if (start == end) {
return null;
}
ListNode slow = start;
ListNode fast = start;
while (fast != end && fast.next != end) {
slow = slow.next;
fast = fast.next.next;
}
TreeNode root = new TreeNode(slow.val);
root.left = helper(start, slow);
root.right = helper(slow.next, end);
return root;
}
判断单链表是否为回文链表
回文链表的定义:从head出发和从tail(反向)出发遍历,得到的数值一样。
例子:
1->2->1
,输出:true
1->2->2->1
,输出:true。
1->2->3
,输出:false。
public static boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head;
ListNode quick = head;
while (quick != null && quick.next != null) {
quick = quick.next.next;
slow = slow.next;
}
ListNode pre = null;
ListNode p = slow;
while (p != null) {
ListNode temp = p.next;
p.next = pre;
pre = p;
p = temp;
}
while (pre != null) {
if (pre.val == head.val) {
pre = pre.next;
head = head.next;
} else {
return false;
}
}
return true;
}