力扣初级算法(三)【链表】

力扣初级算法(三)【链表】

链表问题相对容易掌握。 不要忘记 "双指针解法" ,它不仅适用于数组问题,而且还适用于链表问题。

另一种大大简化链接列表问题的方法是 "Dummy node" 节点技巧 ,所谓 Dummy Node 其实就是带头节点的指针。

我们推荐以下题目:反转链表,合并两个有序链表和链接环。

更有额外的挑战,你可以尝试运用 递归 来解决这些问题:反转链表,回文链表和合并两个有序链表。

  • 在开始题目之前,我们先给出链表中节点的定义。
/* Definition for singly-linked list. */
public class ListNode 
{
    public int val;
    public ListNode next;
    public ListNode(int val=0, ListNode next=null) 
    {
        this.val = val;
        this.next = next;
    }
}

206. 反转链表

难度:简单

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶:

你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

解题思路

  • 朴素的想法是,遍历这个链表,并借助一个额外的容器来存放我们遍历的节点,然后反向输出,把这些节点再串联起来即可。

  • 当然,我们并不需要那么多额外空间来储存节点,完全可以在遍历的同时完成反转的操作。

    • 我们先从一个较小规模的问题开始研究:

      例如:1->2->NULL

    • 反转这个链表很简单,我们只需要让 节点2 指向节点1,然后再让节点1指向NULL即可。

      Node(2).next -> Node(1)
      Node(1).next -> NULL
      
    • 为什么要让节点1指向NULL呢?因为节点1原本是指向节点2的,

      如果不断开这个链接,就会陷入到无限循环当中

    • 现在你已经知道如何反转链表了,让我们扩大一下问题规模:

      例如:1->2->3->4->NULL

    • 我们应该让节点4 指向节点3节点3指向节点2,以此类推。

      最后别忘了让节点1指向NULL

  • 这看起来像是一个从尾到头的操作,如果使用递归,我们不难写出答案,

    不过在这之前,我们应该考虑一下迭代的写法,如何在遍历的过程中反转链表呢?

  • 我们不难想到要这样遍历一个链表:

    for (var node = head; node != null; node= node.next)
    {
    	/* Do Something */
    }
    
    • 但是在将当前节点的next指针指向前一个节点之后,

      我们就无法利用这个节点的next指针向后遍历了,

      而且我们也不知道当前节点的前一个节点是谁。

    • 朴素的想法是,引入另外两个指针来指向当前节点的前一个节点和后一个节点,以此来完成反转和遍历。

方法一:迭代

public ListNode ReverseList(ListNode head)
{
    ListNode l = null;

    for (ListNode c = head, r = c.next; c != null; l = c, c = r, r = r?.next)
        c.next = l;
    
    return l;
}
  • 递归的写法也很简单,我们只需要先利用递归找到尾部节点,

    然后在返回的过程中进行反转操作即可。

  • 由于我们要将尾部节点当做新的头结点返回,所以每次return的都是尾部节点。

  • 最后,由于递归的过程中我们并不知道前一个节点是谁,

    我们只知道当前节点和它的next指针,

    所以我们让当前节点的next指针指向当前节点,

    同时要让当前节点指向NULL避免循环。

方法二:递归

public ListNode ReverseList(ListNode head)
{
    if (head?.next is null) return head; // 递归的终点

    var res = ReverseList(head.next); // 下一步
    
    head.next.next = head;
    head.next = null;

    return res; // 返回的结果
}

21. 合并两个有序链表

难度:简单

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

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

解题思路

  • 由于两个链表都是升序的,我们无需排序,

    只要比较两个链表的当前节点,选择较小的一个加入到新的链表当中即可。

    newList.Add(Min(l1.val, l2.val))
    
  • 在迭代当中,我们将利用temp指针来完成遍历,让temp.next 指向较小的节点。

方法一:迭代

public ListNode MergeTwoLists(ListNode l1, ListNode l2)
{
    var head = new ListNode();
    var temp = temp;

    for (; l1 != null && l2 != null; temp = temp.next)
    {
        if (l1.val < l2.val)
        {
            temp.next = l1;
            l1 = l1.next;
        }
        else
        {
            temp.next = l2;
            l2 = l2.next;
        }
    }
    temp.next = l1 is null ? l2 : l1;

    return head.next;
}
  • 在递归当中,我们改变了原有的链表的结构,每次都找到一个较小的节点,然后将它们拼接起来。

方法二:递归

public ListNode MergeTwoLists(ListNode l1, ListNode l2)
{
    if (l1 is null) return l2;
    if (l2 is null) return l1; 

    if (l1.val < l2.val)
    {
        l1.next = MergeTwoLists(l1.next, l2);
        return l1;
    }
    else
    {
        l2.next = MergeTwoLists(l1, l2.next);
        return l2;
    }
}

141. 环形链表

难度:简单

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环, 我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1 ,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

示例 1:

img

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。 

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos-1 或者链表中的一个 有效索引

解题思路

  • 朴素的想法是,用额外的空间来记录访问过的节点,
    如果再次访问到已经访问到的节点,则说明链表有环。
  • 例如,可以使用集合来判断访问的节点是否已经访问过。

方法一:集合判重

public bool HasCycle(ListNode head)
{
    var set = new HashSet<ListNode>();
    for (var temp = head; temp != null; temp = temp.next)
        if (!set.Add(temp)) return true;

    return false;
}
  • 我们还可以考虑快慢指针的解法,如果链表中有环,
    意味着不断访问next指针,最后会陷入一个循环当中。

  • 我们可以想象成在一个环形跑道上,跑的快的人和跑的慢的人同时出发,

    随着时间的推移,跑的快的人一定会追上跑的慢的人。

    也就是说,快慢指针相遇即判定链表有环。

  • 如果跑的快的人先到达了终点,说明不存在循环跑道,即链表无环。

方法二:快慢指针

public bool HasCycle(ListNode head) 
{
    for (ListNode q = head?.next, l = head; q != null; q = q.next?.next, l = l.next)
        if (q.Equals(l)) return true;

    return false;
}

234. 回文链表

难度:简单

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false

示例 2:

输入: 1->2->2->1
输出: true

进阶:

你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

解题思路

  • 朴素的想法是,利用双指针来判断是否是回文。

  • 但是对于链表来说,我们不知道尾部节点是哪一个,也无法直接从后往前遍历,

    朴素的做法是将链表转换成列表,然后就很好判断了。

方法一:转换

public bool IsPalindrome(ListNode head)
{
    var list = new List<int>();
    for (var i = head; i != null; i = i.next)
        list.Add(i.val);

    for (int i = 0, j = list.Count - 1; i < j; i++, j--)
        if (list[i] != list[j]) return false;
    
    return true;
}
  • 我们可以利用递归来从后往前遍历,同时从头结点开始从前往后遍历,两者依次进行比较即可。

方法二:递归

public bool IsPalindrome(ListNode head)
{
    return FindP(head);

    bool FindP(ListNode node)
    {
        if (node is null) return true; // 递归的终点

        if (FindP(node.next) && node.val == head.val)
        {
            head = head.next; // 从前往后
            return true;
        }

        return false;
    }
}

最后

  • 通过这些题目的练习,我想你应该已经发现递归和迭代其实是在做相同的事情,只不过二者的遍历方式有所不同罢了。
posted @ 2020-10-15 14:13  惟手熟尔  阅读(185)  评论(0编辑  收藏  举报