算法通关村第二关——终于学会链表反转了

大家好,我是怒码少年小码。

这一篇咱们就搞一个东西——反转链表。因为这家伙实在是太常见了。

示例:

  • 输入:[1,2,3,4,5]
  • 输出:[5,4,3,2,1]

你会怎么做?

学习笔记

结点的定义:

struct LinkNode {
	int elem;
	LinkNode* next;
};

1. 带虚拟头结点的反转(头插法)

反转链表无非就是链表之间的插入,但我们知道链表插入中头结点和其他结点不一样,为了和普通结点一致,我们只好再建一个“头结点”。

创建一个用于遍历链表的指针,从原链表的第二个结点开始,把遍历指针指向的结点插入到虚拟头结点之后(头插法):

算法通关村第二关——终于学会链表反转了_第1张图片

怎么样,是不是十分明朗呀。

 LinkNode* reserveList(LinkNode* head) {
    //虚拟头结点
	  LinkNode* virHead = new LinkNode;
    //遍历指针
	  LinkNode* cur = head;
	  virHead->next = head;
      
	  while (cur != nullptr) {
		  LinkNode* next = cur->next;
		  cur->next = virHead->next;
		  virHead->next = cur;
		  cur = next;
	  }
   //返回真实的头结点
	  return virHead->next;
}

值得注意的是本例在while循环体中,很多人第一次会写成这样:

//错误写法!!!
while (cur != nullptr) {
    cur->next = virHead->next;
    virHead->next = cur;
    cur = cur->next;
}

这样写的本意是:既然当前结点已经插到新链表中,那我就更新一下当前遍历结点的指向,让它指向下一个结点继续插入。

但是,这是错误的。我们都知道当前遍历结点的下一个结点地址只保存在cur-next中,当前遍历结点需要插入新链表中,要改变指针域cur->next = virHead->next这时cur->next的值已经变了

所以在插入前要用一个新的变量保存一下cur->next的值(例如本例中的next),再当前指针插入完之后,继续更新遍历结点(本例中的cur = next;)

2. 不带虚拟头结点(穿针引线法)

强烈推荐!!这种方法更难,但也会更有水平。先观察一下前后的变化:

算法通关村第二关——终于学会链表反转了_第2张图片

我们通常需要三个指针来保存已经反转完的结点、当前结点和下一个要反转的结点。原理如下图:

算法通关村第二关——终于学会链表反转了_第3张图片
代码示例:

LinkNode* reserveList2( LinkNode* head ) {
	 LinkNode* prev = nullptr;
	 LinkNode* cur = head;

	 while (cur != nullptr) {
		    LinkNode* next = cur->next;
		    cur->next = prev;
		    prev = cur;
		    cur = next;
	 }
	 return prev;
}

详细实现过程:

  1. 声明两个指针 prevcur 并初始化为 nullptrhead
  2. 进入循环,遍历链表结点。
  3. 在循环中,先保存当前结点的下一个结点为next。(理由和第一种方法的一样)
  4. 将当前结点的指针域指向前一个结点 prev,实现指针反转。
  5. 更新 prev 指针为当前结点 cur
  6. 更新 cur 指针为下一个结点 next
  7. 继续循环直到遍历完整个链表。
  8. 当循环结束时,链表已反转完成,最后返回反转后的链表头结点 prev

通过将每个节点的 next 指针指向前一个节点,不断更新 prevcur 指针,最终实现了链表的反转。

3.拓展——递归实现反转

LinkNode* reserveList3( LinkNode* head ) {
  if (head == NULL || head->next == NULL) {
	  	   return head;
	  }
	  LinkNode* newHead = reserveList3(head->next);

	  head->next->next = head;
	  head->next = nullptr;

	  return newHead;
}
  1. 在递归函数 reserveList3 中,传入链表的头节点 head
  2. 在递归的过程中,先将 head 的下一个节点作为参数传入递归函数,并将返回值保存在 newHead 中。这一步的作用是将链表的头部节点放到了反转后链表的尾部。
  3. 在递归回溯过程中,当到达链表的最后一个节点时,会开始从链表的尾部开始反转操作⭐。
  4. 将当前节点 head 的下一个节点的下一个节点指向当前节点 head,即 head->next->next = head,实现指针的反转操作。
  5. 将当前节点 head 的下一个节点指向空指针,即 head->next = nullptr,断开当前节点 head 与下一个节点的连接,避免形成环。
  6. 返回存放了反转后链表头节点的 newHead

注意:由于递归使用了系统栈,所以链表过长时可能会导致栈溢出的风险。因此,递归反转链表时应谨慎使用。

END

对了,前面两种方法我都没写判断链表是否为空或者只有一个结点的条件,主要是为了代码简洁便于理解,毕竟这两种情况就不用反转了(直接返回head)。

你可能感兴趣的:(算法学习,算法,链表,数据结构)