链表是面试中最常问的问题之一,问完项目经验和基础知识后,面试官通常让你写个手写链表算法题,比如阿里的面试官经常问的一个问题就是手写一个链表反转和如何判断一个链表存在环。
以下为链表的数据结构
public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } }
1)链表反转
链表反转需要将1->2->3变为1<-2<-3,即将下一个节点的next指向当前节点
public static ListNode reverseList(ListNode head) { ListNode node = head; ListNode pre = null; while (node != null) { ListNode tmp = node.next; node.next = pre; pre = node; node = tmp; } return pre; }
2)链表中是否有环
这个问题有多种解法,我这里就简单列举两种
1.判重法:循环遍历链表,遍历到的节点标记下来,只要发现标记过的节点再次出现,就说明链表中存在环
public boolean hasCycle(ListNode head) { Set<ListNode> listNodes = new HashSet<>(); while (head != null) { if (listNodes.contains(head)) { return true; } else { listNodes.add(head); } head = head.next; } return false; }
2.快慢指针:一个指针走一次经过两个节点,另一个走一次经过一个节点,如果没有环的话,两个指针不会相遇,快指针会走到链表的末尾
public boolean hasCycle(ListNode head) { ListNode qucik = head; ListNode slow = head; while (qucik != null && qucik.next != null) { qucik = qucik.next.next; slow = slow.next; if (qucik == slow) { return true; } } return false; }
3)链表排序
排序算法在链表中的使用
1.冒泡排序
public static ListNode bubbleSort(ListNode head) { if (head == null || head.next == null) { return null; } ListNode temp = head; ListNode tail = null; while (head.next != tail) { while (head.next != tail) { if (head.val > head.next.val) { int tmp = head.val; head.val = head.next.val; head.next.val = tmp; } head = head.next; } tail = head; head = temp; } return head; }
2.快排
public static void quickSort(ListNode head) { if (head == null || head.next == null) { return; } quickSort(head, null); } private static void quickSort(ListNode head, ListNode end) { if (head != end && head.next != end) { ListNode p = partition(head, end); quickSort(head, p); quickSort(p.next, end); } } private static ListNode partition(ListNode head, ListNode end) { int baseVal = head.val; ListNode node = head; ListNode cur = head.next; while (cur != end) { if (cur.val < baseVal) { node = node.next; int temp = node.val; node.val = cur.val; cur.val = temp; } cur = cur.next; } int temp = node.val; node.val = head.val; head.val = temp; return node; }
4)两个链表相加1->2->3 + 4->5->6->7=5->7->9->7
private static ListNode addListNode(ListNode node1, ListNode node2) { ListNode head = new ListNode(0); ListNode tmpNode = head; while (node1 != null && node2 != null) { ListNode node = new ListNode(node1.val + node2.val); tmpNode.next = node; node1 = node1.next; node2 = node2.next; } while (node1 != null) { ListNode node = new ListNode(node1.val); tmpNode.next = node; node1 = node1.next; } while (node2 != null) { ListNode node = new ListNode(node2.val); tmpNode.next = node; node2 = node2.next; } return head.next; }
5)获取链表倒数第n个节点
public ListNode nNodeBeforeTail(ListNode head, int n) { if (head == null || n < 1) { return null; } ListNode l1 = head; ListNode l2 = head; for (int i = 0; i < n - 1; i++) { if (l2 == null) { return null; } l2 = l2.next; } while (l2.next != null) { l1 = l1.next; l2 = l2.next; } return l1; }
6)获取链表的中间节点
快慢指针,p1每次走两步,p2每次走一步,链表长度为奇数则返回size/2+1,链表长度为偶数则返回size/2
public static ListNode getMidNode(ListNode head) { if (head == null) { return null; } ListNode p1 = head; ListNode p2 = head; while (p1.next != null) { if (p1 == null) { break; } p1 = p1.next.next; p2 = p2.next; } return p2; }
7)倒叙打印链表
使用栈,先进后出
public static void reversePrintNode(ListNode node) { Stack<ListNode> stack = new Stack<>(); while (node != null) { stack.push(node); node = node.next; } while (!stack.isEmpty()) { System.out.println(stack.pop().val); } }
8)合并2个排序过的链表
public static ListNode mergeNodes(ListNode list1, ListNode list2) { ListNode result = new ListNode(0); ListNode tail = result; while (list1 != null && list2 != null) { if (list1.val < list2.val) { tail.next = list1; tail = tail.next; list1 = list1.next; } else { tail.next = list2; tail = list2; list2 = list2.next; } } if (list1 != null) { tail.next = list1; } else { tail.next = list2; } return result.next; }
9)判断两个链表是否相交
如果两个链表相交,两个链表最后一个节点就相等
public static boolean isIntersect(ListNode head1, ListNode head2) { ListNode p1 = head1; ListNode p2 = head2; while (p1.next != null) { p1 = p1.next; } while (p2.next != null) { p2 = p2.next; } return p1 == p2; }
10)求一个带有环的链表中,进入环的第一个节点
快慢指针可以判断一个链表是否有环, p1 每次两步,p2 每次一步,当 p1 和 p2 重合后,让 p1 回到链表的头部,重新走,每次步长不是走2了,而是走1,那么当 p1 和 p2 再次相遇的时候,就是环路的入口了。
原因:假定起点到环入口点的距离为 a,p1 和 p2 的相交点M与环入口点的距离为b,环路的周长为L,当 p1 和 p2 第一次相遇的时候,假定 p2 走了 n 步。
p1走的路径: a+b+kL = 2n(p1 每次两步,p2 每次一步)
p2走的路径: a+b = n
根据上述公式可得
k*L=a+b=n
a=L - b(k为遍历环的次数,无论遍历多少次环都会回到相同的结点,所以k可以忽略)
当p1从M开始行动时,剩下的距离就为L-b,而p2从链表的第一个结点开始走到环入口的距离为a,由于a=L-b,因此当两指针相遇时相遇结点变为入环的第一个结点。
public ListNode getFirstNodeInCycle(ListNode head) { if(head == null) { return null; } ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if(fast == slow){ slow = head; while(slow != fast){ slow = slow.next; fast = fast.next; } return slow; } } return null; }