在不使用任何指针的情况下反转单个链接列表

时间:2013-07-23 20:59:09

标签: c pointers linked-list

当我说不使用任何指针时,我的意思是我们仍然使用'next'指针字段来遍历列表,但是在反转链接列表时不会更改它们。

据我所知,似乎有办法解决这个问题:

  • 一种方法是在不改变指针本身的情况下反转节点中的数据。
  • 第二个是创建一个新的链表,它与原始链表相反。

如果有人能帮助我如何继续第一种方法,我将不胜感激。其他方法也受到欢迎。

6 个答案:

答案 0 :(得分:4)

此问题与任何其他逆转问题没有太大区别。一种解决方案是遍历列表,并将每个数据项放入数组中。然后,再次遍历列表,但从数组的末尾开始,然后以相反的顺序用数组中的值覆盖列表数据。

for (n in list) arr[i++] = n->data
for (n in list) n->data = arr[--i]

如果不允许存储任何内容,则递归也会消失,因为它充当辅助数组来存储反转列表。然后一个愚蠢的解决方案是实现列表的随机访问接口:

Node * nth_list_item (Node *list, int n);

返回列表中的n th 项。然后,您使用此接口反转列表,以便像数组一样访问它(当然会有时间损失)。逆转不再需要O(n)时间,但现在是O(n 2 )。


如果递归是可以的,那么为了满足“不存储元素”要求的精神,你需要递归地遍历列表,然后在你展开时反转列表。这可以通过允许递归调用的另一个参数来提供在递归调用从末尾展开时提供遍历列表开头所需的指针来实现。此实现在每次递归调用时不使用额外的局部变量,只使用参数中提供的变量。

void swap_left (node *a, node *b, int tmp) {
    a->data = b->data;
    b->data = tmp;
}

void reverse_recursively (node *cur, node **tail) {
    if (cur) {
        reverse_recursively(cur->next, tail);
        if (*tail) {
            swap_left(cur, *tail, cur->data);
            if (cur != *tail) *tail = (*tail)->next;
            if (cur == *tail) *tail = 0;
        }
    }
}

void reverse (node *list) {
    reverse_recursively(list, &list);
}

如果我们被允许在使用递归时蚕食“不存储元素”要求的精神,那么有一个更直接(但更需要空间)的解决方案。基本上,可以在递归遍历它时创建反向链表的副本。到达它的末尾时,可以再次遍历列表,复制反转副本中的元素。

#define make_node(n, d) (node){ n, d }

void reverse_recursively (node *list, node *cur, node copy) {
    if (!cur) {
        for (cur = © cur; cur = cur->next, list = list->next) {
            list->data = cur->data;
        }
        return;
    }
    reverse_recursively(list, cur->next, make_node(&copy, cur->data));
}

void reverse (node *list) {
    if (list == 0) return;
    reverse_recursively(list, list->next, make_node(0, list->data));
}

答案 1 :(得分:4)

今天在工作中,我一直回到这个问题,试图理解它。我发现你的限制有点莫名其妙,因此“你试过魔法吗?”评论。最终我越过了街区......

可能有助于可视化问题。让我们从Julio Moreno的代码中的想法开始,稍微简化一下:逐步完成列表并用尾数据交换每个节点数据。

A B C D E
E B C D A
E A C D B
E A B D C
E A B C D

E D B C A
E D A C B
E D A B C

E D C B A

(在工作中我总结了这个过程不会有效,但现在我有更多时间可以看到它)。如果我们仔细看看他的函数,我们可以看到,除此之外,它还可以递归地工作。我并不热衷于可视化从for循环调用的递归函数。这个过程显然不会赢得任何效率奖。

那么让我们来看看

A B C D E
  B C D E A
    C D E B A
      D E C B A
        E D C B A

这里我们采用尾节点E,并记住它。我们现在接受节点A并在E之后插入它,然后在B之后插入它,但在A之后,在E之后立即插入整个列表,直到E是列表的第一个节点(头部)。它有效,但我们不允许这样做。

让我们更进一步假装它是一个双向链表,我们保持两个指针,一个在列表的开头,一个在最后,我们交换两个数据,然后递增一个,分别减少另一个。

A B C D E
E B C D A
E D C B A

已经完成!

那么我们怎么能用单链表来做呢?我们需要知道什么?我们怎样才能在向前迈进的同时倒退呢?

让我们从如何通过遍历整个列表获得最后一个节点开始。

A B C D E F G H

交换它们:

H B C D E F G A

然后如果我们记住我们交换 数据的节点,我们可以从B开始并一步一步,直到node-> next指向现在持有数据A的节点。

B C D E F G

并交换它们:

G C D E F B
  F D E C
    E D 

然而,我仍然对重复踩到列表的想法感到不安 - 即使步进的范围在每次迭代过程中缩小。如果我们有一个LIFO(后进先出)或堆叠,如果它已经知道怎么办?

A B C D E F G H
  B C D E F G H ... A
    C D E F G H ... B
      D E F G H ... C
        E F G H ... D
          F G H ... E
            G H ... F
              H ... G...F...E...D...C...B...A

但那是辅助数据存储,我们不允许这样做,但是看看如何使用递归函数调用和链表实现LIFO并不是很困难。那么我们怎么能用递归函数调用和链表来前进和后退呢?我们不需要额外的参数吗?当我们到达列表的末尾时,我们仍然需要知道它是如何开始的。

A B C D E F G H
A,B             ^ return 0
  A,C           ^ return 0
    A,D         ^ return 0
      A,E       ^ swap D E done, return 0
        A,F     ^ swap C F return D
          A,G   ^ swap B G return C
            A,H ^ swap A H return B

我实际上还没有对此进行过测试,以证明它可能是错误的。我现在就去测试它,如果要求可以发布代码。希望我不必编辑这篇文章说它不起作用; - )

编辑:可以确认它有效。

static lnode* list_private_reverse(lnode* list, lnode* node)
{
    lnode* next = node->next;

    if (next)
    {
        lnode* swap = list_private_reverse(list, next);

        if (swap)
        {
            int c = swap->c;

            swap->c = node->c;
            node->c = c;

            if (swap->next == node || swap->next->next == node)
                return 0;

            return swap->next;
        }
        return 0;
    }
    else
    {
        int c = node->c;
        node->c = list->c;
        list->c = c;
    }

    return list->next;
}

lnode* list_reverse(lnode* list)
{
    list_private_reverse(list, list);
    return list;
}

list_private_reverse的调用次数与列表中的元素一样多。

答案 2 :(得分:0)

这样的东西可以工作,第一个函数实际上没有切换(抱歉令人困惑的名字),它的工作方式与插入算法非常相似,它采用列表的最后一个元素并将其插入“当前”位置。我对此算法的效率存在严重怀疑:

    typedef struct node node;
    struct node {
        node *next;
        void *value;
    };
    typedef node linked_list;

    node *switch_with_end(node *c)
    {
        if (c->next) {
            node *t = switch_with_end(c->next);
            void *temp = t->value;
            t->value = c->value;
            c->value = temp;
        }
        return c;
    }

    void reverse_list(linked_list *l)
    {
        node *c;
        for (c = l; c->next; c = c->next)
            switch_with_end(c);
    }

答案 3 :(得分:0)

@ames Morris - 很好地解释道。但是你和我的代码之间会有什么不同......

最多使用2个指针......

Node* reverseLL (Node *curr, Node *prev)
{
  Node *nxt = NULL;
  if (curr)
  {
    nxt = curr->next;
    curr->next = prev;
    curr = reverseLL (nxt, curr);
  }
  else
    return prev;
}

void reverseList (Node **head)
{
  Node *curr = *head;
  Node *prev = NULL;
  curr = reverseLL (curr, prev);
  *head = curr;
}

答案 4 :(得分:0)

实现堆栈并使用它来反转链表。 Stack是先进先出数据结构(FILO)。在第一次迭代中将链接列表的内容推送到堆栈,然后在第二次迭代期间将它们弹回到列表中。

实施堆栈后,它可以很好地解决大部分逆转问题。

当然,你使用更多的空间,这意味着它不在原地。

答案 5 :(得分:0)

 //Reversal of linked list without using pointers.
void reverseList(ListNode* head) {
    if(head == NULL || head->next == NULL)
        return head;
    reverseList(head->next);
    //save the current value
    int val = head->val;
    ListNode *temp = head;
    // shift all the values to the left
    while(temp->next != NULL)
    {
        temp->val = temp->next->val;
        temp = temp->next;
    }
    // assign the save value at the end of the list
    temp->val = val;
    return;
}