当前位置:   article > 正文

面试精选之链表(上)_面试链表

面试链表

学习算法的一个目的就是为了通过面试,拿下期望的offer,链表做为一个基础的数据结构,除了基础的链表实现外,在面试过程中,有一些问题的出现概率非常的高,本篇主要是对链表相关的面试题进行汇总,给出具体的过程分析以及最终实现。

这里在啰嗦一下的是,在对链表操作的过程中,要时刻保证指针(有的语言称为引用)操作有效,避免无效操作。要做到这一点就要深刻理解指针的概念。另外,本篇中,所有的问题都是以单链表为例的,因为这些问题只有以单链表为情景时,才具有可分析性。

 

好了,本篇计划讲解以下5个例子。

  1. 链表反转
  2. 链表中环的检测
  3. 合并2个有序链表
  4. 查找链表的中间节点
  5. 查找链表中倒数第k个元素

本篇代码中,用到的单链表的节点定义如下

  1. /// <summary>
  2. /// 单链表节点
  3. /// </summary>
  4. public class SNode<T>where T:IComparable<T>
  5. {
  6. public T Value { get; set; }
  7. public SNode<T> Next { get; set; }
  8. public SNode(T val)
  9. {
  10. Value = val;
  11. }
  12. }

下面我们就开始进入正文。

 

链表反转

链表反转在面试中出现的概率非常之高,虽然最终实现相对也比较简单,但是,想要写出Bug free的代码却不是很容易,主要就是在指针的操作上容易出错,导致指针丢失,首先,我们先来看一下正常的解法思路。

假设存在链表 1 → 2 → 3 → Ø,我们想要把它改成 Ø ← 1 ← 2 ← 3,对于单链表,我们只知道当前节点的下一个节点是谁,要想反转整个链表,正常来说,我们要考虑将当前节点的下一个节点,指向当前节点,依次循环,直到到达链表的末尾,可以结合下面的图片来理解一下。

现在,我们试着来理一下处理过程,我们可以借助3个指针来处理,用curNode指针指向当前的节点,next指向当前节点的下一节点,用指针pre指向当前节点的上一节点。当反转时,我们可以将curNode.next=pre,这样即完成了相邻的2个节点的反转,然后,将指针后移进行下一步操作即可。talk is cheap,下面给出代码,你可以结合看看。

这种常规解法,我们再来看另一种解法思路。

要反转链表,我们可以创建一个哨兵节点,然后遍历链表,将遍历到的元素依次插入到链表的第一个节点。即哨兵节点的下一个节点,这样,循环结束后,哨兵节点指向的节点即为反转后链表的头节点,如1-2-3-4-5 -->1-3-2-4-5  -->1-4-3-2-5  -->1-5-4-3-2  -->5-4-3-2-1,我们也称这种方法为头插法。下面来看一下代码。

  1. public SNode<T> Reverse()
  2. {
  3. SNode<T> p, q;
  4. SNode<T> Head = new SNode<T>(default(T));
  5. p = Head.Next;
  6. while (p.Next != null)
  7. {
  8. q = p.Next;
  9. p.Next = q.Next;
  10. q.Next = Head.Next;
  11. Head.Next = q;
  12. }
  13. p.Next = Head;
  14. Head = p.Next.Next;
  15. p.Next.Next = null;
  16. return Head;
  17. }

链表中环的检测

如果链表中有环,那如果用一个指针在链表上循环,则将一直循环下去,那如果我们用2个指针呢?如果用一快一慢两个指针来循环,若链表存在环,那么快慢指针终将会在环上相遇。这里用到的即是快慢指针的思想,有了这个思想,实现还是很简单的,我们来看下具体实现。

  1. public bool HasCycle()
  2. {
  3. SNode<T> slow = Head;
  4. SNode<T> fast = Head.Next;
  5. while (fast != null && fast.Next != null)
  6. {
  7. slow = slow.Next;
  8. fast = fast.Next.Next;
  9. if (slow == fast) return true;
  10. }
  11. return false;
  12. }

合并2个有序链表

对于2个有序链表,因为链表链表是有序的,因此我们可以依次来取出比较,我们可以用2个指针分别指向2个链表并在链表上进行移动,在比较的过程中依次将取得的结点组成一条新的链表。当有一个指针移动到链表尾时,再将另一条链表剩下的结点连接到新建列表尾即可。如图,

那现在,我们来试着实现一下。

  1. public SNode<int> mergeTwoLists(SNode<int> l1, SNode<int> l2)
  2. {
  3. SNode<int> soldier = new SNode<int>(0);//利用哨兵节点简化实现难度
  4. SNode<int> p = soldier;//用来存储当前指向的指针
  5. while (l1 != null && l2 != null)
  6. {
  7. if (l1.Value < l2.Value)
  8. {
  9. p.Next = l1;
  10. l1 = l1.Next;
  11. }
  12. else
  13. {
  14. p.Next = l2;
  15. l2 = l2.Next;
  16. }
  17. p = p.Next;
  18. }
  19. if (l1 != null) p.Next = l1;
  20. if (l2 != null) p.Next = l2;
  21. return soldier.Next;//新链表的头节点
  22. }

查找链表的中间结点

我们知道,当链表有奇数个结点时,中间结点为一个结点,当链表有偶数个结点时,我们以返回前一个结点来分析。这里,还是用到快慢指针的思想,我们分别将快慢指针slow和fast指向链表的头结点,然后slow每次移动一个结点,fast每次移动2个结点,那么,当fast结点到达链表尾的时候,slow结点即为我们要求的中间结点。我们来分析下,当链表为奇数项时,最后一次fast指针指向链表尾结点,此时,slow指向中间结点。当链表为偶数项时,最后一次,fast.next指针指向链表尾结点,同样的,此时slow刚好指向链表中间位置的前一项。

代码如下:

  1. public SNode<T> findMiddleNode(SNode<T> list)
  2. {
  3. if (list == null) return null;
  4. SNode<T> fast = list;
  5. SNode<T> slow = list;
  6. while (fast.Next != null && fast.Next.Next != null)
  7. {
  8. fast = fast.Next.Next;
  9. slow = slow.Next;
  10. }
  11. return slow;
  12. }

若是,要返回后一项作为中间结点,那我们得分情况来返回响应值,如下:

  1. public ListNode findMiddleNode(ListNode head)
  2. {
  3. if (head == null) return head;
  4. ListNode fast = head;
  5. ListNode slow = head;
  6. while (fast.next != null && fast.next.next != null)
  7. {
  8. fast = fast.next.next;
  9. slow = slow.next;
  10. }
  11. if (fast.next == null)//奇数
  12. {
  13. return slow;
  14. }
  15. else//偶数项:fast.next.next==null
  16. {
  17. return slow.next;
  18. }
  19. }

删除链表倒数第k个结点

对于单链表,我们并不能从后向前查找结点,那如何才能找到倒数第k个结点的位置呢?还记得我们分析前几个问题时的快慢指针吗?如果我们用2个指针分别指向链表的头结点,然后让其中的一个指针先移动k步,每次移动一个结点,然后,从此时开始,2个结点同时移动,都是每次移动一步,那么,当先移动的指针到达链表尾时,后移动的指针是不是刚好指向倒数第k个位置呢?没错,这个问题的求解思想就是这样的。如图:

(动图这里是先让快指针移动k+1步,然后再2个指针同时移动,但终止条件是fast==null,和先移动k步,然后判断fast.next==null是一样的道理。)

那么,我们来看一下具体的代码实现

  1. public SNode<T> deleteLastKth(SNode<T> list, int k)
  2. {
  3. SNode<T> fast = list;
  4. SNode<T> slow = list;
  5. int i = 1;
  6. while (fast != null && i < k)
  7. {
  8. fast = fast.Next;
  9. i++;
  10. }
  11. if (fast.Next == null) { list = list.Next; }
  12. while (fast.Next != null)
  13. {
  14. fast = fast.Next;
  15. slow = slow.Next;
  16. }
  17. slow.Next = slow.Next.Next;
  18. return list;
  19. }

 

至此,我们的5个问题就讲解完成了,其实,除了我上述讲解的思路,很多问题是可以通过递归的思想来处理的,比如说上述的链表反转、合并2个有序链表问题,如果,你对递归比较了解,可以试着分析一下这两个问题的递归实现,没有也没关系,等随后章节讲解递归时,可以拿这2个问题来进行分析。

 

其实,链表的算法,考察的主要是思维模式而不是代码实现,不管何种算法,其实主要的是要掌握其分析思想,另外,想要写出Bug free的代码,就是要多写多练。俗话说,书读便便其义自见、眼过千遍不如手过一遍都是一样的道理。希望这篇文章对大家能有帮助。

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

闽ICP备14008679号