力扣初级算法(三)【链表】
力扣初级算法(三)【链表】
链表问题相对容易掌握。 不要忘记
"双指针解法",它不仅适用于数组问题,而且还适用于链表问题。另一种大大简化链接列表问题的方法是
"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:
输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。示例 2:
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。示例 3:
输入:head = [1], pos = -1 输出:false 解释:链表中没有环。提示:
- 链表中节点的数目范围是
[0, 104]-105 <= Node.val <= 105pos为-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;
}
}
最后
- 通过这些题目的练习,我想你应该已经发现递归和迭代其实是在做相同的事情,只不过二者的遍历方式有所不同罢了。




浙公网安备 33010602011771号