单向链表反转算法——递归版和迭代版

  最近在做笔试题时,遇到一道编程题:单向链表反转算法。一时紧张,没写出来就提前交卷了,然而交完卷就想出来了。。。

  最初想出来的是递归版,遗憾的是没能做到尾递归,后来又琢磨出了迭代版。后来用实际编译运行测试了一遍,能正常运行。

  递归版的灵感来源于《Haskell 趣学指南》中非常简洁的快速排序算法的实现,其思想是将单向链表分割头部和尾部。其中头部指是链表的第一个节点,尾部是指除去第一个节点后的子链表。通过递归的方法,将子链表继续分割成头部和尾部,直至尾部指剩下一个节点,无法继续分割,然后将头部和尾部的位置交换,返回尾部,然后将前一个头部接到返回的尾部后面,继续返回新的尾部,依此类推,直到整个递归完成。

  迭代版的思想是,用三个指针(first, now, next)来协助调整顺序。其中每一次迭代(即:每遍历一个节点),都将单链表动态分割成反转后的和反转前的两部分。每一次迭代后,反转后的链表会变长,反转前的链表会变短,直到反转前的链表长度为0,反转完成,返回反转后的链表。整个思想和冒泡排序有点像,把序列(链表)划分成有序区(反转后的部分)和无序区(反转前的部分),但是只需对整个单向链表进行一趟遍历。

  值得注意的是,第一次迭代,是从单向链表的第二个节点开始的。而第一个节点默认就是反转后的节点,而且是反转后的尾节点,为了避免环路导致的死循环,需要将该节点指向的下一个节点的指针设为 NULL 。

  这三个指针的作用如下:

  • first 指针始终指向反转后的链表的首节点;
  • now 指针指向当前遍历到的反转前的链表的首节点;
  • next 指针指向 now 节点的下一个节点,避免在 now->next 指向反转后的链表首节点时,丢失了反转前的链表的引用。

关键数据结构如下:

1 typedef struct _Node {
2     int data;
3     struct _Node * next;
4 } Node;

递归版的反转算法代码如下:

 1 Node * Node_reverse(Node *node) {
 2     if (node == NULL) return NULL;
 3     if (node->next == NULL) return node;
 4     Node * n = Node_reverse(node->next);
 5     if (n != NULL) {
 6         n->next = node;
 7         node->next = NULL;
 8     }
 9     return node;
10 }

其实,这个递归版反转算法有个小缺陷:那就是整个递归完成时,返回的节点就是传入的参数。也就是说,如果传入的是反转前的单向链表首节点,那么最终返回的也是这个节点,而这个节点却是反转后的单向链表的尾节点(因为反转了,首尾交换)。因此,使用时,需要做些微不足道的工作来弥补缺陷。即:在使用前需要先对单向链表进行一趟遍历,找到尾节点并用指针引用它。。。其实还能改善一下这段代码来修正这个小缺陷,把这些微不足道的工作交给这个函数来完成,也就是加个 if 分支的事,我懒得写了。迭代版的就没这个问题,而且效率略高。

迭代版的反转算法代码如下:

 1 Node * Node_reverse_v2(Node* node) {
 2     if (node == NULL) return NULL;
 3     if (node->next == NULL) return node;
 4
 5     Node *first = node; //总是指向新链表的首部。
 6     Node *now = node->next;
 7     Node *next = now->next;
 8     first->next = NULL; //首节点变成尾节点,尾节点的下一个节点置空,防止环路。
 9     do {
10         next = now->next;
11         now->next = first;
12         first = now;
13         now = next;
14     } while (next != NULL);
15
16     return first;
17 }

测试代码如下:

 1 void printNode(Node *node) {
 2     if (node != NULL) {
 3         printf("%d ", node->data);
 4     }
 5 }
 6
 7 int main()
 8 {
 9     LinkedList *l = List_create();
10     for (int i=0; i<20; i++) {
11         List_append(l, i);
12     }
13     List_foreach(l, printNode);
14     printf("\n");
15     List_foreach(List_reverse(l), printNode);
16     printf("\n");
17     List_foreach(List_reverse_v2(l), printNode);
18     printf("\n");
19     return 0;
20 }

输出如下:

完整代码下载:

reverse.7z

时间: 2025-01-13 12:08:00

单向链表反转算法——递归版和迭代版的相关文章

小菜鸟的单向链表反转

关于单向链表的反转想必大家都很熟了,以前一直用递归的方法写的,还是挺好写的,但是后来又在网上瞄了一眼非递归方法的实现,当时以为那个代码是正确的,也没验证,后来就不了了之. 昨天下午开始写这个代码,最后老发现出问题,今天早上起来理了一遍,终于发现症结所在. 举个例子吧: 1->2->3->4->5->6       我反转后的结果应该是: 6->5->4->3->2->1 我的算法是这样的(语死早,大家不要笑,我就用过程图表达吧): 1->2

单向链表反转,就地逆置与递归反转(无表头结点)

最近在看链表,今天刷到一道链表的反转题,链表反转可以说是基础操作,但是可提供的方案也有很多,简单通过了该题后又学习了一下递归反转,现在把三种方法都公开出来做一个总结. 1.就地逆置 2.单参数的递归逆置 3.双参数的递归逆置 一.就地逆置 方法:头插. 由于这里是不带表头结点的单向链表,所以头插会稍微复杂一点,不想往下看的小伙伴也可以直接选择定义一个临时表头结点从头结点开始遍历链表将每一个链表头插,最后将头结点指向表头结点的next指针域,最后free掉那个表头结点即可. 虽然不带表头结点头插会

链表反转的递归和非递归实现方式

链表反转是数据结构的基本功,主要有递归和非递归两种实现方式.我们一一介绍如下: 1. 非递归实现 主要包括如下4步: 1)如果head为空,或者只有head这一个节点,return head即可: 2)从头到尾遍历链表,把reversedHead赋值给当前节点的next: 3)当前节点赋值给reversedHead: 4)遍历结束,return reversedHead. 下图试图来辅助说明: 代码如下: node* reverseList(node* head) { if(head == NU

链表反转(递归方式,非递归方式)

//非递归方式进行链表反转 public ListNode reverseList(ListNode head){ if(head==null||head.next==null){ return head; }else { ListNode pre=head; ListNode p=head.next; ListNode next=null; while (p!=null) { next=p.next; p.next=pre; pre=p; p=next; } head.next=null; r

链表反转(递归与非递归实现)

复习一下链表反转 #include <stdio.h> #include <malloc.h> typedef int ElemType; typedef struct Node { int data; struct Node* next; }Node, *List; //用数组arr来初始化链表中数据:此例中链表无头点 int InitList(List *list, ElemType* arr, int num) { int i= 0; Node* tail_node; Nod

php 单向链表反转 reverse (没有空的头结点)

* 参照php标准库设计接口 http://php.net/manual/en/class.spldoublylinkedlist.php * 反转单向链表 reverse方法, 其他的方法为了方便测试 <?php /** * Created by PhpStorm. * User: Mch * Date: 8/11/18 * Time: 00:25 */ class Node { public $value; public $next; public function __construct(

数据结构-单向链表相关算法

#include <stdio.h>#include <stdlib.h>#define OVERFLOW -2#define OK 1#define ERROR 0typedef int ElemType;//单向链表结构体typedef struct LNode {    ElemType data;    struct LNode *next;}LNode,*LinkList; LinkList CreateList_L(LinkList L,int n);void Trav

Java实现单向链表反转

环境: Java: jdk1.8.0_91 public class LinkedListTest { public static void main(String[] args) { Node A = new Node("A"); Node B = new Node("B"); Node C = new Node("C"); Node D = new Node("D"); Node E = new Node("E&

数据结构和算法--3链表(单向链表、双向链表、环形单向链表和约瑟夫问题)

链表 链表是以节点的方式来存储 每个节点包含data域和next域,指向下一个节点 链表的各个节点不一定是连续存储 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定 单向列表 最大特点是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能. 单链表分为两种:有头链表和无头链表. 有头节点的增删改查 定义一个单链表的类: //定义一个SingleLinkedList,单链表,管理HeroNode class SingleLinkedList{ //初始