- 链表面试题常用数据结构和技巧
- 常见面试题
- 1)给定一个单链表的头结点head,请判断该链表是否为回文结构。
- 2)将单项链表按某值划分成左边小、中间相等、右边大的形势
- 3)一种特殊的单链表节点类描述如下,给定一个由Node节点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表的复制,并返回复制的新链表头节点。要求时间复杂度O(N),额外空间复杂度O(1)
- 4)给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点,如果不相交,返回null。【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
- 5)能不能不给单链表的头结点,只给想要删除的结点,就能做到在链表上把这个点删掉?
面试时,链表解题方法论:
1、对于笔试, 不用太在乎空间复杂度,一切为了时间复杂度
2、对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法
链表面试题常用数据结构和技巧
1、使用容器(哈希表、数组等)
2、快慢指针
1)输入链表头结点,奇数长度返回中点,偶数长度返回上中点
/*** 快慢指针从头出发,都从奇数位开始走* 这样快指针每次走两步后都在奇数位上* 这样在偶数长度链表上,当快指针不能往下走的时候,慢指针正好在上中点*/public Node midOrUpMidNode(Node head) {if (head == null || head.next == null) {return head;}//慢指针Node slow = head;//快指针Node fast = head;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
2)输入链表头结点,奇数长度返回中点,偶数长度返回下中点
/*** 快慢指针从第二个结点出发,都从偶数位开始走* 这样快指针每次走两步后都在偶数位上* 这样在偶数长度链表上,当快指针不能往下走的时候,慢指针正好在下中点*/public Node midOrDownMidNode(Node head) {if (head == null || head.next == null) {return head;}Node slow = head.next;Node fast = head.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
3)输入链表头结点,奇数长度返回中点前一个,偶数长度返回上中点前一个
public Node findMid(Node head) {if (head == null || head.next == null || head.next.next == null) {return head;}Node slow = head;Node fast = head.next.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
4)输入链表头结点,奇数长度返回中点前一个,偶数长度返回下中点前一个
public Node findMid(Node head) {if (head == null || head.next == null) {return null;}if (head.next.next == null) {return head;}Node slow = head;Node fast = head.next;while (fast.next != null && fast.next.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}
常见面试题
1)给定一个单链表的头结点head,请判断该链表是否为回文结构。
public boolean isPalindrome1(Node head) {Stack<Node> stack = new Stack<Node>();Node cur = head;while (cur != null) {stack.push(cur);cur = cur.next;}while (head != null) {if (head.value != stack.pop().value) {return false;}head = head.next;}return true;}
//实现思路//1.先找出中点(偶数个是找出上中点)//2.将中点的下个节点指向空,从中点的下个节点开始反转列表//3.然后从新链表的头和原链表的头开始遍历链表并比较,全部相等则是回文,否则不是//4.恢复原链表结构(将反转的链表再次反转为原始状态)// need O(1) extra spacepublic static boolean isPalindrome3(Node head) {if (head == null || head.next == null) {return true;}Node n1 = head;Node n2 = head;while (n2.next != null && n2.next.next != null) { // find mid noden1 = n1.next; // n1 -> midn2 = n2.next.next; // n2 -> end}// n1 中点n2 = n1.next; // n2 -> right part first noden1.next = null; // mid.next -> nullNode n3 = null;while (n2 != null) { // right part convertn3 = n2.next; // n3 -> save next noden2.next = n1; // next of right node convertn1 = n2; // n1 moven2 = n3; // n2 move}n3 = n1; // n3 -> save last noden2 = head;// n2 -> left first nodeboolean res = true;while (n1 != null && n2 != null) { // check palindromeif (n1.value != n2.value) {res = false;break;}n1 = n1.next; // left to midn2 = n2.next; // right to mid}n1 = n3.next;n3.next = null;while (n1 != null) { // recover listn2 = n1.next;n1.next = n3;n3 = n1;n1 = n2;}return res;}
2)将单项链表按某值划分成左边小、中间相等、右边大的形势
public static Node listPartition1(Node head, int pivot) {if (head == null) {return head;}Node cur = head;int i = 0;while (cur != null) {i++;cur = cur.next;}Node[] nodeArr = new Node[i];i = 0;cur = head;for (i = 0; i != nodeArr.length; i++) {nodeArr[i] = cur;cur = cur.next;}arrPartition(nodeArr, pivot);for (i = 1; i != nodeArr.length; i++) {nodeArr[i - 1].next = nodeArr[i];}nodeArr[i - 1].next = null;return nodeArr[0];}public static void arrPartition(Node[] nodeArr, int pivot) {int small = -1;int big = nodeArr.length;int index = 0;while (index != big) {if (nodeArr[index].value < pivot) {swap(nodeArr, ++small, index++);} else if (nodeArr[index].value == pivot) {index++;} else {swap(nodeArr, --big, index);}}}
public static Node listPartition2(Node head, int pivot) {Node sH = null; // small headNode sT = null; // small tailNode eH = null; // equal headNode eT = null; // equal tailNode mH = null; // big headNode mT = null; // big tailNode next = null; // save next node// every node distributed to three listswhile (head != null) {next = head.next;head.next = null;if (head.value < pivot) {if (sH == null) {sH = head;sT = head;} else {sT.next = head;sT = head;}} else if (head.value == pivot) {if (eH == null) {eH = head;eT = head;} else {eT.next = head;eT = head;}} else {if (mH == null) {mH = head;mT = head;} else {mT.next = head;mT = head;}}head = next;}// 小于区域的尾巴,连等于区域的头,等于区域的尾巴连大于区域的头if (sT != null) { // 如果有小于区域sT.next = eH;eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT}// 下一步,一定是需要用eT 去接 大于区域的头// 有等于区域,eT -> 等于区域的尾结点// 无等于区域,eT -> 小于区域的尾结点// eT 尽量不为空的尾巴节点if (eT != null) { // 如果小于区域和等于区域,不是都没有eT.next = mH;}return sH != null ? sH : (eH != null ? eH : mH);}
3)一种特殊的单链表节点类描述如下,给定一个由Node节点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表的复制,并返回复制的新链表头节点。要求时间复杂度O(N),额外空间复杂度O(1)
class Node { int value; Node next; Node rand; Node(int val) {value = val;} }
public static Node copyListWithRand1(Node head) {if (head == null) {return null;}Map<Node, Node> cloneMap = new HashMap<>();Node node = head;while (node != null) {cloneMap.put(node, new Node(node.value));node = node.next;}node = head;while (node != null) {cloneMap.get(node).next = cloneMap.get(node.next);cloneMap.get(node).rand = cloneMap.get(node.rand);node = node.next;}return cloneMap.get(head);}
public static Node copyRandomList2(Node head) {if (head == null) {return null;}Node cur = head;Node next = null;// 1 -> 2 -> 3 -> null// 1 -> 1' -> 2 -> 2' -> 3 -> 3'while (cur != null) {next = cur.next;cur.next = new Node(cur.val);cur.next.next = next;cur = next;}cur = head;Node copy = null;// 1 1' 2 2' 3 3'// 依次设置 1' 2' 3' random指针while (cur != null) {next = cur.next.next;copy = cur.next;copy.random = cur.random != null ? cur.random.next : null;cur = next;}Node res = head.next;cur = head;// 老 新 混在一起,next方向上,random正确// next方向上,把新老链表分离while (cur != null) {next = cur.next.next;copy = cur.next;cur.next = next;copy.next = next != null ? next.next : null;cur = next;}return res;}
4)给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点,如果不相交,返回null。【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
public static Node getIntersectNode1(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}Set<Node> set1 = new HashSet<>();Node curr = head1;while (curr != null && !set1.contains(curr)) {set1.add(curr);curr = curr.next;}Set<Node> set2 = new HashSet<>();curr = head2;while (curr != null && !set2.contains(curr)) {if (set1.contains(curr)) {return curr;}set2.add(curr);curr = curr.next;}return null;}
//主函数public static Node getIntersectNode(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}//分别找出两个链表的第一个入环结点Node loop1 = getLoopNode(head1);Node loop2 = getLoopNode(head2);//两个链表无环,可能相交(在某个结点相遇后,后续部分公用),可能不相交if (loop1 == null && loop2 == null) {return noLoop(head1, head2);}//两个链表有环,可能不相交,如果相交存在两种情况//1、在同一个结点入环,相交结点一个//2、不同的结点入环,相交结点两个(返回任一相交结点)if (loop1 != null && loop2 != null) {return bothLoop(head1, loop1, head2, loop2);}//一个有环一个无环,不可能相交return null;}// 找到链表第一个入环节点,如果无环,返回null// 隐藏技巧:快慢指针第一次相遇后,快指针回到头结点,然后快慢指针每次都走一步,最后相遇结点一定是入环结点public static Node getLoopNode(Node head) {if (head == null || head.next == null || head.next.next == null) {return null;}// n1 慢 n2 快Node slow = head.next; // n1 -> slowNode fast = head.next.next; // n2 -> fastwhile (slow != fast) {if (fast.next == null || fast.next.next == null) {return null;}fast = fast.next.next;slow = slow.next;}// slow fast 相遇fast = head; // n2 -> walk again from headwhile (slow != fast) {slow = slow.next;fast = fast.next;}return slow;}// 如果两个链表都无环,返回第一个相交节点,如果不想交,返回nullpublic static Node noLoop(Node head1, Node head2) {if (head1 == null || head2 == null) {return null;}Node cur1 = head1;Node cur2 = head2;int n = 0;while (cur1.next != null) {n++;cur1 = cur1.next;}while (cur2.next != null) {n--;cur2 = cur2.next;}if (cur1 != cur2) {return null;}// n : 链表1长度减去链表2长度的值cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1cur2 = cur1 == head1 ? head2 : head1; // 谁短,谁的头变成cur2n = Math.abs(n);while (n != 0) {n--;cur1 = cur1.next;}while (cur1 != cur2) {cur1 = cur1.next;cur2 = cur2.next;}return cur1;}// 两个有环链表,返回第一个相交节点,如果不相交返回nullpublic static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {Node cur1 = null;Node cur2 = null;if (loop1 == loop2) {cur1 = head1;cur2 = head2;int n = 0;while (cur1 != loop1) {n++;cur1 = cur1.next;}while (cur2 != loop2) {n--;cur2 = cur2.next;}cur1 = n > 0 ? head1 : head2;cur2 = cur1 == head1 ? head2 : head1;n = Math.abs(n);while (n != 0) {n--;cur1 = cur1.next;}while (cur1 != cur2) {cur1 = cur1.next;cur2 = cur2.next;}return cur1;} else {cur1 = loop1.next;while (cur1 != loop1) {if (cur1 == loop2) {return loop1;}cur1 = cur1.next;}return null;}}
//使用方法Node[] nodes = generateRandomLinkedList(len, val);Node intersectNode1 = nodes[2];Node intersectNode2 = nodes[3];Node intersectNode = getIntersectNode(nodes[0], nodes[1]);if (intersectNode1 == null && intersectNode != null) {throw new RuntimeException("error");}if (intersectNode1 != null && intersectNode != intersectNode1&& intersectNode != intersectNode2) {throw new RuntimeException("error");}// for test 生成随机长度的相交或不相交链表并返回相交结点(有相交的情况)// 返回数组第一个为链表1的头结点// 第二个为链表2的头结点// 第三个为第一个相交的结点(只有一个相交结点时也在此返回)// 第四个为第二个相交的结点(只有真实存在两个相交结点时才返回)private static Node[] generateRandomLinkedList(int len, int maxValue) {int length1 = (int) (Math.random() * len) + 1;Node[] arr1 = new Node[length1];for (int i = 0; i < length1; i++) {arr1[i] = new Node((int) (Math.random() * maxValue) + 1);}int length2 = (int) (Math.random() * len) + 1;Node[] arr2 = new Node[length2];for (int i = 0; i < length2; i++) {arr2[i] = new Node((int) (Math.random() * maxValue) + 1);}for (int i = 0; i < arr1.length - 1; i++) {if (i < length1 - 1) {arr1[i].next = arr1[i + 1];}}Node intersect1 = null;Node intersect2 = null;switch ((int) ((Math.random() * 5)) + 1) {case 1: //两个无环链表无相交for (int i = 0; i < arr2.length; i++) {if (i < length2 - 1) {arr2[i].next = arr2[i + 1];}}System.out.println("两个无环链表无相交");break;case 2: //两个无环链表相交int i1 = (int) (Math.random() * length1);int i2 = (int) (Math.random() * length2);for (int i = 0; i < arr2.length; i++) {if (i < length2 - 1) {arr2[i].next = arr2[i + 1];}if (i == i2) {arr2[i].next = arr1[i1];break;}}intersect1 = arr1[i1];System.out.println("两个无环链表相交");break;case 3: //一个有环链表一个无环链表,不存在相交for (int i = 0; i < arr2.length; i++) {if (i < length2 - 1) {arr2[i].next = arr2[i + 1];}}i2 = (int) (Math.random() * length2);arr2[arr2.length - 1].next = arr2[i2];System.out.println("一个有环链表一个无环链表,不存在相交");break;case 4: //两个有环链表无相交i1 = (int) (Math.random() * length1);arr1[arr1.length - 1].next = arr1[i1];for (int i = 0; i < arr2.length; i++) {if (i < length2 - 1) {arr2[i].next = arr2[i + 1];}}i2 = (int) (Math.random() * length2);arr2[arr2.length - 1].next = arr2[i2];System.out.println("两个有环链表无相交");break;case 5: //两个有环链表相交i1 = (int) (Math.random() * length1);i2 = (int) (Math.random() * length2);for (int i = 0; i < arr2.length; i++) {if (i < length2 - 1) {arr2[i].next = arr2[i + 1];}if (i == i2) {arr2[i].next = arr1[i1];break;}}if (arr2[i2].next == null) {System.out.println();}intersect1 = arr1[i1];i1 = (int) (Math.random() * length1);arr1[arr1.length - 1].next = arr1[i1];intersect2 = arr1[i1];System.out.println("两个有环链表相交");}return new Node[]{arr1[0], arr2[0], intersect1, intersect2};}private static class Node {int value;Node next;public Node(int value) {this.value = value;}@Overridepublic String toString() {return "Node{" + "value=" + value + '}';}}
5)能不能不给单链表的头结点,只给想要删除的结点,就能做到在链表上把这个点删掉?
此题有问题,(错误解法,将下个节点的值设置到给定的节点)无法删除末尾节点。 要删除指定的node还是需要给头结点。
