如何使用仅两个指针反转单向链表?

114

我想知道是否存在一种逻辑可以仅使用两个指针来反转单链表。

以下方法使用三个指针,分别为pqr,用于反转单链表:

struct node {
    int data;
    struct node *link;
};

void reverse() {
    struct node *p = first,
                *q = NULL,
                *r;

    while (p != NULL) {
        r = q;
        q = p;
        p = p->link;
        q->link = r;
    }
    first = q;
}

有没有其他替代方法来反转链表?在时间复杂度方面,反转单向链表的最佳逻辑是什么?


1
可能重复:https://dev59.com/tEfRa4cB1Zd3GeqP83D8 - kajaco
3
不完全正确,这是两个队列而不是两个指针。 - paxdiablo
7
因为你在这里是来帮忙的,而不是为了玩一场声望游戏。 - GManNickG
1
你正在帮助那些从问题和答案中获益的读者,我发现这非常有见地。 - Andrew Coleson
1
双向链表可以在O(1)时间内实现反转。请查看我的答案。 - Karussell
显示剩余2条评论
36个回答

135

有什么替代方案吗?没有,这已经是最简单的方法了,也没有根本上不同的做法。该算法已经是O(n)时间复杂度,你不能再更快了,因为你必须修改每个节点。

看起来你的代码走在了正确的轨道上,但它在上面的形式中还不太能工作。这是一个可行的版本:

#include <stdio.h>

typedef struct Node {
  char data;
  struct Node* next;
} Node;

void print_list(Node* root) {
  while (root) {
    printf("%c ", root->data);
    root = root->next;
  }
  printf("\n");
}

Node* reverse(Node* root) {
  Node* new_root = 0;
  while (root) {
    Node* next = root->next;
    root->next = new_root;
    new_root = root;
    root = next;
  }
  return new_root;
}

int main() {
  Node d = { 'd', 0 };
  Node c = { 'c', &d };
  Node b = { 'b', &c };
  Node a = { 'a', &b };

  Node* root = &a;
  print_list(root);
  root = reverse(root);
  print_list(root);

  return 0;
}

2
以上代码中有没有错误?在 while 循环内部,每次都会创建一个新的“next”指针。因此,如果链表中有 N 个节点,则会创建 N 个新指针,并且您没有释放或删除它们。我认为,如果您在 while 循环之前创建“next”指针,并在 while 循环内部进行赋值“next = root->next”,那么就可以正确执行。 - aks
8
@aks表示没有泄漏。 注意未调用malloc等函数,因此不需要释放。 变量'next'的作用域限于循环内部,但这完全可以接受。 - Roger Pate
1
即使没有泄漏,每次都声明下一个的必要性是什么呢?正如aks所提到的,“如果在while循环之前创建'next'指针并在while循环内部进行赋值'next = root->next',那么这样做是正确的”,不是吗? - Jagdish
1
我喜欢你的链表字面量,很整洁。 - user755921
1
@Jagdish - 在循环中声明变量是没有成本的,限制变量的作用域是一个好主意。查看使用在循环中定义的变量和在循环外定义的变量版本的优化汇编输出。 - Jonathan Leffler
显示剩余2条评论

44

我很不愿意成为传递坏消息的人,但我认为您提出的三指针解决方案实际上并不起作用。当我在下面的测试环境中使用它时,列表被缩减为一个节点,如下所示:

==========
4
3
2
1
0
==========
4
==========

由于您的解决方案的时间复杂度为O(n),而且必须访问每个节点才能更改指针,因此您不会获得比您的解决方案更好的时间复杂度,但是您可以很容易地使用仅两个额外指针的解决方案,如下面的代码所示:

#include <stdio.h>

// The list element type and head.

struct node { 
    int data;
    struct node *link;
};
static struct node *first = NULL;

// A reverse function which uses only two extra pointers.

void reverse() {
    // curNode traverses the list, first is reset to empty list.
    struct node *curNode = first;
    first = NULL;

    // Until no more in list, insert current before first and advance.
    while (curNode != NULL) {
        // Need to save next node since we're changing the current.
        struct node *nxtNode = curNode->link;

        // Insert at start of new list.
        curNode->link = first;
        first = curNode;

        // Advance to next.
        curNode = nxtNode;
    }
}

// Code to dump the current list.

static void dumpNodes() {
    struct node *curNode = first;
    printf ("==========\n");
    while (curNode != NULL) {
        printf ("%d\n", curNode->data);
        curNode = curNode->link;
    }
}

// Test harness main program.

int main (void) {
    int i;
    struct node *newnode;

    // Create list (using actually the same insert-before-first
    // that is used in reverse function.

    for (i = 0; i < 5; i++) {
        newnode = malloc (sizeof (struct node));
        newnode->data = i;
        newnode->link = first;
        first = newnode;
    }

    // Dump list, reverse it, then dump again.

    dumpNodes();
    reverse();
    dumpNodes();
    printf ("==========\n");

    return 0;
}

这段代码输出:

==========
4
3
2
1
0
==========
0
1
2
3
4
==========

我认为这就是你所需要的。实际上它可以做到这一点,因为一旦你将first加载到遍历列表的指针中,你可以随意重复使用first


2
非常优雅。在链表本身上重复使用 first 指针使得该解决方案只需要使用 2 个 额外 指针,但总共仍需要 3 个指针。 - Kevin Kibler
你在这里使用了first、curNode和nxtNode三个指针,为什么这被称为是一个两个指针的解决方案? - Yash
@Yash,请再读一遍,first 顶部有两个 额外 指针。就像 OP 的三指针解决方案中有 firstpqr 一样。 - paxdiablo
@paxdiablo 哦!我的错。抱歉,我误解了问题。谢谢 :) - Yash

25
#include <stddef.h>

typedef struct Node {
    struct Node *next;
    int data;
} Node;

Node * reverse(Node *cur) {
    Node *prev = NULL;
    while (cur) {
        Node *temp = cur;
        cur = cur->next; // advance cur
        temp->next = prev;
        prev = temp; // advance prev
    }
    return prev;
}

2
你好!我知道这个问题很老了,但是你介意解释一下这个函数发生了什么以及为什么它能够工作吗? :) 谢谢! - MakeTheTrumpetsBlow

13

以下是用 C 语言翻转单向链表的代码,可在此处查看。

下面是代码的复制粘贴:

// reverse.c

#include <stdio.h>
#include <assert.h>

typedef struct node Node;
struct node {
    int data;
    Node *next;
};

void spec_reverse();
Node *reverse(Node *head);

int main()
{
    spec_reverse();
    return 0;
}

void print(Node *head) {
    while (head) {
        printf("[%d]->", head->data);
        head = head->next;
    }
    printf("NULL\n");
}

void spec_reverse() {
    // Create a linked list.
    // [0]->[1]->[2]->NULL
    Node node2 = {2, NULL};
    Node node1 = {1, &node2};
    Node node0 = {0, &node1};
    Node *head = &node0;

    print(head);
    head = reverse(head);
    print(head);

    assert(head == &node2);
    assert(head->next == &node1);
    assert(head->next->next == &node0);

    printf("Passed!");
}

// Step 1:
//
// prev head  next
//   |    |    |
//   v    v    v
// NULL  [0]->[1]->[2]->NULL
//
// Step 2:
//
//      prev head  next
//        |    |    |
//        v    v    v
// NULL<-[0]  [1]->[2]->NULL
//
Node *reverse(Node *head)
{
    Node *prev = NULL;
    Node *next;

    while (head) {
        next = head->next;
        head->next = prev;
        prev = head;
        head = next;
    }

    return prev;
}

4
谢谢您为解释提供的精美ASCII艺术品 :) - achedeuzot

4

罗伯特·塞奇威克,《C语言算法》(第三版),Addison-Wesley出版社,1997年,[第3.4节]

In case that is not a cyclic list ,hence NULL is the last link.

typedef struct node* link;

结构体节点(node):

struct node{ int item; link next; };

/* 输入一个已存在的链表到reverse()函数,返回倒序后的链表 */

链表逆序函数(link reverse): link reverse(link x){ link t, y = x, r = NULL; while(y != NULL){ t = y->next; y->next = r; r = y; y = t; } return r; }


注意是否将指针定义为typedef的好主意?——简短回答:不是。 - Jonathan Leffler

3

仅供娱乐(虽然尾递归优化应该能防止它耗尽所有的栈):


Node* reverse (Node *root, Node *end) {

    Node *next = root->next;
    root->next = end;

    return (next ? reverse(next, root) : root);
}

root = reverse(root, NULL);

2
我认为“应该”这个词用得有点过了。你的 C 编译器 “可能” 会进行尾调用优化,而且很容易检查一个给定编译器/选项是否进行了优化:看看反汇编代码就行了。或者给它几百万个节点,看看它是否会崩溃;-) - Steve Jessop

3
你需要一个跟踪指针来追踪列表。
你需要两个指针:
第一个指针选择第一个节点。第二个指针选择第二个节点。
处理过程: 移动跟踪指针 将第二个节点指向第一个节点 通过将第二个指针分配给一,将第一个指针移动一步 通过将跟踪指针赋值为第二个指针,将第二个指针移动一步
Node* reverselist( )
{
   Node *first = NULL;  // To keep first node
   Node *second = head; // To keep second node
   Node *track =  head; // Track the list

    while(track!=NULL)
    {
      track = track->next; // track point to next node;
      second->next = first; // second node point to first
      first = second; // move first node to next
      second = track; // move second node to next
    }

    track = first;

    return track;

}


3

是的。我确定你可以用和不使用第三个变量交换两个数字一样的方法来完成这个操作。只需将指针强制转换为int/long类型,然后执行几次异或操作即可。这是那些有趣但没有实际价值的C技巧之一。

你能减少O(n)的复杂度吗?实际上不能。如果你认为需要反向排序,只需使用双向链表即可。


...并且如果你不小心,就会出现一个新的64位兼容性问题。这样做也不太可能提高性能。 - Left For Archive
2
这不会影响时间复杂度 - 也就是说,它不会使解决方案比线性时间更好。我的意思是,你可能会节省4或8个字节的内存,但这不会改变算法的总体复杂度。 - poundifdef
@rascher,时间复杂度是问题的第二部分。第一部分与减少所需指针的数量有关。 - paxdiablo
2
我认为原帖作者正在寻找一种廉价的C语言技巧。根据我的经验 - 我已经进行了分析 :) - 典型的避免中介技巧实际上比使用中介更慢。 - Will
链接已经失效,但我确信使用异或交换两个数字是老派的做法 :) - Dane
你可以在常数时间内完成它,伙计。 - user1095108

2

以下是Java语言的简化版本。它只使用了两个指针currprev

public void reverse(Node head) {
    Node curr = head, prev = null;

    while (head.next != null) {
        head = head.next; // move the head to next node
        curr.next = prev; //break the link to the next node and assign it to previous
        prev = curr;      // we are done with previous, move it to next node
        curr = head;      // current moves along with head
    }

    head.next = prev;     //for last node
}

这个问题需要用C语言解决,而不是Java。 - Degustaf
1
这个问题更多地涉及到只使用两个额外的指针(或引用)来执行反向操作。无论是C还是Java,逻辑都是相同的。 - ernesto

2
更易读的选项如何呢:

Node *pop (Node **root)
{
    Node *popped = *root;

    if (*root) {
        *root = (*root)->next;
    }

    return (popped);
}

void push (Node **root, Node *new_node)
{
    new_node->next = *root;
    *root = new_node;
}


Node *reverse (Node *root)
{
    Node *new_root = NULL;
    Node *next;

    while ((next = pop(&root))) {
        push (&new_root, next);
    }

    return (new_root);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接