当前位置:   article > 正文

[Java]手撕链表面试题及双向链表(进阶)_java 双向链表 面试问题

java 双向链表 面试问题

 

目录

前言

一.链表面试题

1. 删除链表中等于给定值 val 的所有节点。

2. 反转一个单链表。 

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

4. 输入一个链表,输出该链表中倒数第k个结点。

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

 6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

7. 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 

8. 链表的回文结构。(进阶)

9. 输入两个链表,找出它们的第一个公共结点。

10. 给定一个链表,判断链表中是否有环。 

11. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null (进阶)

二.无头双向链表的实现

1.基本类的配置:

2.基本功能实现:

1)打印链表:

2)获取链表的长度:

3)链表中是否包含某个元素:

4)找到要删除或插入的元素:

1)头插法:

2)尾插法:

3)任意位置插:

4)删除第一次出现的元素(key):

5)删除所有出现过的元素(key):

6)清空链表:

总结



前言

        Hellow,大家好!我是Node_Hao!熟练掌握顺序表及链表的相关操作之后,便可以适当用一些较高难度的链表笔试题巩固提高自己的知识水平,不仅是对原有知识的复习提升,更是打破自己知识舒适区的最佳方式.另外本章还会讲到双向链表的相关知识,毕竟Java的集合框架库中LinkedList底层实现就是无头双向循环链表.所以双向链表的重要性不言而喻.


一.链表面试题

1. 删除链表中等于给定值 val 的所有节点。

解题步骤:

        这道题的做法类似于侦查兵探路,首先我们定义一个prev节点和cur节点,prev节点类似长官拥有删除功能,cur类似于士兵只负责探路,有危险(要删的元素)cur向前走,prev长官的指向直接跳过该元素,当cur发现安全(没有要删的元素时),prev再向前挪动.

  1. public ListNode removeAllKey(int key) {
  2. if (head == null) {
  3. System.out.println("单链表不能为空!");
  4. return null;
  5. }
  6. ListNode prev = head;
  7. ListNode cur = head.next;
  8. while (cur != null) {
  9. if (cur.val == key) {
  10. prev.next = cur.next;
  11. cur = cur.next;
  12. } else {
  13. prev = cur;
  14. cur = cur.next;
  15. }
  16. }
  17. if (head.val == key) {
  18. head = head.next;
  19. }
  20. return head;
  21. }


2. 反转一个单链表。 

        由于链表得天独厚的优势,反转链表只需要改变其各个元素的指向即可,首先我们定义三个变量,三个变量各自有其作用,prev用于充当新的头结点,cur用于改变链表的指向,curNext用于cur向后移动遍历链表,因为cur在其改变指向的同时会丢失向后走的能力.三个变量的步频始终一致,当cur==null时prev刚好走到末尾成为新的头结点.

  1. public ListNode reverseList() {
  2. if (head == null) {
  3. return null;
  4. }
  5. ListNode prev = null;
  6. ListNode cur = head;
  7. while (cur != null) {
  8. ListNode curNext = cur.next;
  9. cur.next = prev;
  10. prev = cur;
  11. cur = curNext;
  12. }
  13. return prev;//新的头结点
  14. }


3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点

         这道题如果只是简单的将链表个数除以二,需要遍历链表两次,那么它必然达不到面试的难度,在面试难度下我们只能将链表遍历一次.因此我们需要用快慢指针来解决这类问题,首先定义快指针fast和慢指针slow,假设一个链表长为5,fast的速度为slow的两倍,那么相同时间内fast的路程必然是slow两倍,因此当fast走到末尾时,slow刚好走到中间位置.最后返回slow即可.那么就会产生一个问题,当链表长度为奇数是必然返回中间位置,那如果链表为偶数时,该返回什么位置呢?题目中要求的是返回第二个节点,如果面试官要求返回第一个呢?其实很简单我们只需要让fast在走的过程中为空时,不然slow继续走直接返回slow即可.

  1. public ListNode middleNode() {
  2. ListNode faster = head;
  3. ListNode slower = head;
  4. while (faster != null && faster.next != null) {
  5. faster = faster.next.next;
  6. slower = slower.next;
  7. }
  8. return slower;
  9. }

                                                                                                        

  1. public ListNode middleNode() {
  2. ListNode faster = head;
  3. ListNode slower = head;
  4. while (faster != null && faster.next != null) {
  5. faster = faster.next.next;
  6. if (faster==null){
  7. return slower;
  8. }
  9. slower = slower.next;
  10. }
  11. return slower;
  12. }


4. 输入一个链表,输出该链表中倒数第k个结点。

         一看到这道题我们第一时间想到的肯定是,算出链表的长度size,然后减去k,走size-k步即可,这样的话我们需要遍历链表两次.而面试难度只允许你遍历一次.此时就需要一点点数学技巧,咋们结合我下面给出的图来看.倒数第k个节点距最后一个节点k-1步,如果fast先走k-1步,那么slow与fast就相差k-1步,fast与slow一人一步走,当fast为最后一个节点时,slow自然而然就是倒数第k个节点,,此时返回slow即可.但是当我们判读k的合法性时,如果想让k不超过链表的长度我们必须遍历链表,这样就得遍历两次链表了.其实我们可以不判断k是否超过链表大小,只需要在fast移动过程中加一条循环条件,一但fast为空直接返回slow即可.

  1. public ListNode FindKthToTail(ListNode head, int k) {
  2. if (head == null) {
  3. return null;
  4. }
  5. if (k <= 0) {
  6. return null;
  7. }
  8. ListNode fast = head;
  9. ListNode slow = head;
  10. while (k - 1 != 0 && fast != null) {
  11. fast = fast.next;
  12. if (fast == null) {
  13. return null;
  14. }
  15. k--;
  16. }
  17. while (fast.next != null) {
  18. slow = slow.next;
  19. fast = fast.next;
  20. }
  21. return slow;
  22. }



5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

        这道题,需要两个链表不断的比较,所以两个链表的头结点需要不断后移达到比较的效果,因此我们需要一个虚拟的头写点(傀儡节点),让之后较小的数连在它后面.当我们合并完所有节点时,只需返回虚拟头节点的下一个节点即可. 

 

  1. public static ListNode mergeTwoLists(ListNode head1, ListNode head2) {
  2. ListNode newHead = new ListNode(-1);//虚拟节点
  3. ListNode tmp = newHead;
  4. while (head1 != null && head2 != null) {
  5. if (head1.val < head2.val) {
  6. tmp.next = head1;
  7. tmp = tmp.next;
  8. head1 = head1.next;
  9. } else {
  10. tmp.next = head2;
  11. tmp = tmp.next;
  12. head2 = head2.next;
  13. }
  14. }
  15. if (head1 == null) {
  16. tmp.next = head2;
  17. }
  18. if (head2 == null) {
  19. tmp.next = head1;
  20. }
  21. return newHead.next;
  22. }
  23. }



6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前

        这道题需要将链表分为两个部分,所以我们定义四个变量,bs,be,as,ae,分别是前半段的首尾和后半段的首尾 .经过判断依次将链表元素放入对于位置,最后再将两个链表合并.这道题有很多需要注意的情况:1.题中并没有说链表是有序排序的,所以当后半段不为空时,可能指针域为空的节点不在最后一位,所以我们需要将ae(最后一位手动置空).2.一定不要忘记链接前后两段.3.当前半段为空时直接返回后半段as即可.

  

  1. public ListNode partition(ListNode pHead, int x) {
  2. ListNode bs = null;
  3. ListNode be = null;
  4. ListNode as = null;
  5. ListNode ae = null;
  6. ListNode cur = head;
  7. while (cur!=null){
  8. if(cur.val<x){
  9. if(bs==null){
  10. bs = cur;
  11. be = cur;
  12. }else {//尾插
  13. be.next = cur;
  14. be = be.next;
  15. }
  16. }else {
  17. if(as==null){
  18. as = cur;
  19. ae = cur;
  20. }else {//尾插
  21. ae.next = cur;
  22. ae =ae.next;
  23. }
  24. }
  25. cur = cur.next;
  26. }
  27. //预防第一个段为空
  28. if (bs == null) {
  29. return as;
  30. }
  31. be.next = as;//前后两段链接
  32. //不一定所有的数字都大于k
  33. if (as != null) {
  34. ae.next = null;
  35. }
  36. return bs;
  37. }



7. 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 

         这道题由于头结点也可能和下一个节点重复,所以需要一个虚拟的头结点,删除思想类似于第一题的侦察兵,cur在前面探路,prev负责删除.值得注意的是,当我们写cur向前走的循环条件时,一定要首先判断cur.next!=null,否则当判断cur.val==cur.next.val时会空指针异常.

  1. public ListNode deleteDuplication(ListNode pHead) {
  2. if(pHead==null){
  3. return null;
  4. }
  5. ListNode newHead = new ListNode(-1);
  6. ListNode prev = newHead;
  7. ListNode cur = pHead;
  8. while (cur!=null){
  9. if(cur.next!=null&&cur.val==cur.next.val){
  10. while (cur.next!=null&&cur.val==cur.next.val){
  11. cur = cur.next;
  12. }
  13. cur = cur.next;
  14. }else {
  15. prev.next = cur;
  16. prev = prev.next;
  17. cur = cur.next;
  18. }
  19. }
  20. prev.next = null;
  21. return newHead.next;
  22. }



8. 链表的回文结构。(进阶)

        这道题堪称经典一题更比三题强,被许多大厂当做面试题,有多种不同的解法,今天我分享的是一种较为容易理解的做法.做法可以概括为三句话"1.找到中间节点,2.反转后半段,3.首尾中间靠拢并判断"1.找到中间节点我们之前已经讲过,用快慢指针这里不过多赘述.2.反转后半段之前也讲过,就是逆置链表的思想.3.首尾中间靠拢并判断是否相等,此时需要注意的是当链表为偶数时,首尾就不会汇聚到同一个节点上,所以我们需要判断当head.next==slow时也跳出循环.

 

  1. public boolean chkPalindrome(ListNode head) {
  2. if (head == null) {
  3. return false;
  4. }
  5. ListNode fast = head;
  6. ListNode slow = head;
  7. ListNode cur = slow.next;
  8. while (fast != null && fast.next != null) {//找到中间位置
  9. fast = fast.next.next;
  10. slow = slow.next;
  11. }
  12. while (cur != null) {//反转链表
  13. ListNode curNext = cur.next;
  14. cur.next = slow;
  15. slow = cur;
  16. cur = curNext;
  17. }
  18. while (head != slow) {//判断回文
  19. if (slow.val != head.val) {
  20. return false;
  21. }
  22. if (head.next != slow) {
  23. return true;
  24. }
  25. slow = slow.next;
  26. head = head.next;
  27. }
  28. return true;
  29. }



9. 输入两个链表,找出它们的第一个公共结点。

        基于链表各个元素之间靠指针域链接的特性,两个链表之所以能够相交,一定是因为指针域相同.一但相交后面就不可能分开,所以链表相交一定是Y型.但是两个链表的长度不一定相同,所以如果我们想找到相同的节点还是得依靠快慢指针,那么快指针比慢指针多走几步呢?由图我们可以发现两个链表的后半段长度一定相等,差别就在前半段.所以我们可以分别计算出两个链表的长度,让快指针先走两个链表的长度的差值步,此时快慢指针所指向的长度一致,然后一人一步走直到相遇,返回相遇节点即可.

        这道题代码看似很长,其实大部分都是计算长度,为了尽可能的少遍历链表,我先假设headA为最长的链表而pl始终指向最长的链表,当计算出的长度小于0时,我们只需交换pl和ps的指向即可. 

  1. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  2. if(headA==null||headB==null){
  3. return null;
  4. }
  5. ListNode pl = headA;
  6. ListNode ps = headB;
  7. int len1 = 0;
  8. int len2 = 0;
  9. while (pl!=null){
  10. len1++;
  11. pl = pl.next;
  12. }
  13. pl = headA;
  14. while (ps!=null){
  15. len2++;
  16. ps = ps.next;
  17. }
  18. ps = headB;
  19. int len = len1-len2;
  20. if(len<0){
  21. pl = headB;
  22. ps = headA;
  23. len = len2 -len1;
  24. }
  25. while (len!=0){
  26. pl = pl.next;
  27. len--;
  28. }
  29. while (pl!=ps){
  30. if(pl==null||ps==null){
  31. return null;
  32. }
  33. pl = pl.next;
  34. ps = ps.next;
  35. }
  36. return pl;
  37. }



10. 给定一个链表,判断链表中是否有环。 

        这道较为简单,它的作用是为下一道题打基础.判断一个链表是否有环,只需定义两个快慢指针,快指针一次走2步,慢指针一次走1步.如果两个指针能相遇那么一定存在环.如果fast在走的过程中为空那么一定不存在环. 

  1. public boolean hasCycle(ListNode head) {
  2. if (head == null) {
  3. return false;
  4. }
  5. ListNode fast = head;
  6. ListNode slow = head;
  7. while (fast != null&&fast.next == null) {
  8. fast = fast.next.next;
  9. slow = slow.next;
  10. if (fast == slow) {
  11. return true;
  12. }
  13. }
  14. return false;
  15. }



11. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null (进阶)

        这道题略有难度,在上一题的基础上我们可以求得相遇点,由我在下图中的分析可得,让fast从起点开始,slow从相遇点开始,一人一步走,一定会在入口点相遇.此时返回入口点即可. 

  1. public ListNode detectCycle(ListNode head) {
  2. if (head == null) {
  3. return null;
  4. }
  5. ListNode fast = head;
  6. ListNode slow = head;
  7. while (fast != null && fast.next != null) {
  8. fast = fast.next.next;
  9. slow = slow.next;
  10. if (fast == slow) {
  11. break;
  12. }
  13. }
  14. if(fast==null||fast.next==null){
  15. return null;
  16. }
  17. fast = head;
  18. while (fast != slow) {
  19. fast = fast.next;
  20. slow = slow.next;
  21. }
  22. return slow;//或者fast
  23. }


二.无头双向链表的实现

        无头双向链表作为,java集合框架中LinkedList的底层实现原理,其重要性不言而喻.今天我们就从最底层的逻辑来构建一个无头双向链表,并实现其基本的增删改查功能.

1.基本类的配置:

        根据双向链表的基本特性,首先我们要定义定义两个基本类,一个是节点类,一个链表类.节点类中存放双向链表节点的基本属性(值域,前驱,后继),这是其面向对象的第一步.其次我们要在链表类中定义一个头结点和为节点,此时的头结点和尾结点只是一个变量,并不是真实存在.这就体现了无头双向链表的特点.最后我们只需要在链表类中构造自己需要的方法即可.

  1. class ListNode2 {
  2. public int val;
  3. public ListNode2 prev;
  4. public ListNode2 next;
  5. public ListNode2(int val) {
  6. this.val = val;
  7. }
  8. }
  9. public class My_DoubleList {
  10. public ListNode2 head;
  11. public ListNode2 last;
  12. }

2.基本功能实现:

1)打印链表:

        定义一个节点类型的变量,在遍历链表的同时打印链表的值即可.

  1. public void display() {
  2. ListNode2 cur = head;
  3. while (cur != null) {
  4. System.out.print(cur.val + " ");
  5. cur = cur.next;
  6. }
  7. }

2)获取链表的长度:

        定义一个节点类型的变量和一个计数器,遍历链表的同时计数器加一即可.

  1. public int size() {
  2. ListNode2 cur = head;
  3. int count = 0;
  4. while (cur != null) {
  5. count++;
  6. cur = cur.next;
  7. }
  8. return count;
  9. }

3)链表中是否包含某个元素:

         定义一个节点类型的变量,遍历链表的同时并判断,如果cur.val==key时,返回true即可.

  1. public boolean contains(int key) {
  2. ListNode2 cur = head;
  3. while (cur != null) {
  4. if (cur.val == key) {
  5. return true;
  6. }
  7. cur = cur.next;
  8. }
  9. return false;
  10. }

4)找到要删除或插入的元素:

        该方法非常重要,插入或删除元素都需要调用它.基于双向链表的特性,我们不论是插入还是删除直接找到当前位置即可,不必像单项链表一样,找到前一个元素的位置.

  1. public ListNode2 searchIndex(int index) {
  2. if (index<0||index>size()){
  3. System.out.println("输入位置不合法!");
  4. return null;
  5. }
  6. ListNode2 cur = head;
  7. while (index!=0){
  8. cur = cur.next;
  9. index--;
  10. }
  11. return cur;
  12. }

以上方法均为辅助功能,接下才是双向链表的真正功能.

1)头插法:

        基于双向链表的特性,当链表为空时,头结点和尾结点都需要置空.当向head之前插入时需改变node.next的指向和head.prev的指向,并将head向前移动.

  1. public void addFirst(int data) {
  2. ListNode2 node2 = new ListNode2(data);
  3. if (head == null) {
  4. head = node2;
  5. last = node2;
  6. } else {
  7. head.prev = node2;
  8. node2.next = head;
  9. head = node2;
  10. }
  11. }

2)尾插法:

        尾插与头插类似这里不过多赘述,之所以没有将node.next置空是因为,node没有初始化时前驱和后继都为null.

  1. public void addLast(int data) {
  2. ListNode2 node2 = new ListNode2(data);
  3. if (head == null) {
  4. head = node2;
  5. last = node2;
  6. } else {
  7. last.next = node2;
  8. node2.prev = last;
  9. last = node2;
  10. }
  11. }

3)任意位置插:

        head==0时调用头插法,head==size()时调用尾插法.不属于以上两种情况时调用查找函数,接收到指定位置的元素cur.在将node与cur相互绑定即可.

  1. public void addIndex(int index, int data) {
  2. if (index < 0 || index > size()) {
  3. System.out.println("输入位置不合法");
  4. return;
  5. }
  6. if (index == 0) {
  7. addFirst(data);
  8. return;
  9. }
  10. if (index == size()) {
  11. addLast(data);
  12. return;
  13. }
  14. ListNode2 cur = searchIndex(index);
  15. ListNode2 node2 = new ListNode2(data);
  16. node2.next = cur;
  17. cur.prev.next = node2;
  18. node2.prev = cur.prev;
  19. cur.prev = node2;
  20. }

4)删除第一次出现的元素(key):

        这道题代码看似很长其实只要搞清每个if-else之间的关系,就能很容易的写出.首先我们需要用变量cur遍历链表,第一个if-else判断cur是不是要删除的元素,如果是暂定,如果不是cur向后移.当第一个if为真时,第二个if再判断你删除的是否为头结点,如果是头结点并且头结点的下一位不为空,我们让头结点向后移即可,但如果头结点的下一位为空,我们得把last也置为空因为last是成员变量.当第二个if为假时,说明删除的不是头结点,为中间节点.但此时还要注意判断我们删除的元素是不是尾结点,如果是尾结点只需让last向后移即可.

 

  1. public void remove(int key) {
  2. ListNode2 cur = head;//局部变量
  3. if (head == null) {
  4. System.out.println("链表为空");
  5. return;
  6. }
  7. while (cur != null) {
  8. if (cur.val == key) {
  9. if (head==cur) {
  10. head = head.next;
  11. if (head!=null){
  12. head.prev = null;
  13. }else {
  14. last = null;
  15. }
  16. } else {
  17. cur.prev.next = cur.next;
  18. if (cur.next != null) {
  19. cur.next.prev = cur.prev;
  20. } else {
  21. last = last.prev;
  22. }
  23. }
  24. return;
  25. } else {
  26. cur = cur.next;
  27. }
  28. }
  29. System.out.println("没有你要删除的元素");
  30. return;
  31. }

5)删除所有出现过的元素(key):

        与删除一个元素方法基本一致,当删除一个元素不必跳出循环,继续删除即可.

  1. public void removeAllKey(int key) {
  2. ListNode2 cur = head;//局部变量
  3. if (head == null) {
  4. System.out.println("链表为空");
  5. return;
  6. }
  7. while (cur != null) {
  8. if (cur.val == key) {
  9. if (cur==head) {
  10. head = head.next;
  11. if (head!=null){
  12. head.prev = null;
  13. }else {
  14. last = null;
  15. }
  16. } else {
  17. cur.prev.next = cur.next;
  18. if (cur.next != null) {
  19. cur.next.prev = cur.prev;
  20. } else {
  21. last = last.prev;
  22. }
  23. }
  24. }
  25. cur = cur.next;
  26. }
  27. return;
  28. }

6)清空链表:

        由于head在置空过程中会迷失方向,所以定义curNext来记录head.next帮助head遍历链表.

  1. public void clear() {
  2. if (head==null){
  3. return;
  4. }
  5. while (head!=null){
  6. ListNode2 curNext = head.next;
  7. head.next = null;
  8. head.prev = null;
  9. head = curNext;
  10. }
  11. last = null;
  12. System.out.println("置空完毕");
  13. return;
  14. }


总结

        顺序表与链表的入门以及进阶内容,今天就更新完了,如果我的文章对你的学习有一点点帮助和启发记得三连哦!祝三连的帅哥美女好运连连!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/661766
推荐阅读
相关标签
  

闽ICP备14008679号