使用递归错误反转链接列表

时间:2015-06-10 01:03:36

标签: c recursion linked-list

我理解递归,所以我尝试编写反向链表程序。我写了下面的函数,但它说分段错误(核心转储)。

void reverse(){
    if (head -> next == NULL){
        return;
    }
    reverse (head -> next);
    struct node *q = (struct node*) malloc (sizeof(struct node));
    q = head -> next;
    q -> next = head;
    head -> next = NULL;
}

请有人指导我。谢谢。

3 个答案:

答案 0 :(得分:2)

不应该反向接受争论吗?请注意,您无法更改函数中的指针并使其成为持久的更改。也就是说,在C函数中,唯一持久的变化是那些使用* var = something的变化。

答案 1 :(得分:1)

递归是一种通过练习获得的思维方式。祝贺你的尝试。这不正确,但不要气馁。

以下是解决问题的两种方法。

您的目标是将其细分为更小版本的自身以及(希望简单快速计算)增量步骤,将较小版本的解决方案转变为完整解决方案。这是递归思维的本质。

首先尝试:将列表视为头元素加上“列表的其余部分”。即,

L = empty or
  = h . R

其中h是头元素R是列表的其余部分,点.正在将新元素加入列表。撤消此列表包括撤消R,然后在结尾添加h

rev(L) = empty if L is empty
       = rev(R) . h otherwise

这是一个递归解决方案,因为我们可以递归调用反向函数来解决反转R的稍小的问题,然后添加一些工作来追加h,这给了我们完整的解决方案

这个公式的问题在于追加h比你想要的更贵。由于我们有一个只有头指针的单链表,所以耗费时间:遍历整个列表。但它会工作正常。在C中它将是:

NODE *rev(NODE *head) {
  return head ? append(head, rev(head->next)) : NULL;
}

NODE *append(NODE *node, NODE *lst) {
  node->next = NULL;
  if (lst) {
    NODE *p;
    for (p = lst; p->next; p = p->next) /* skip */ ;
    p->next = node;
    return lst;
  }
  return node;
}

那么如何摆脱糟糕的表现呢?通常情况是,问题的不同递归公式具有不同的效率。因此经常涉及一些反复试验。

接下来尝试:根据将列表分成两个子列表来考虑计算:L = H T,因此rev(L)= rev(T)+ rev(H)。这里加+是列表连接。关键是,如果我知道rev(H)并想要在其头部添加新元素,则要添加的元素是T中的第一个元素。如果这样似乎模糊,让H = [a,b,c]和T = [d,e]。然后,如果我已经知道rev(H)= [c,b,a]并且想要在头部前面添加下一个元素,我想要d,这是T的第一个元素。在我们的小符号中,你可以写这个观察就是这样:

rev(H + (d . T)) = rev(T) + ( d . rev(H) )

所以这看起来非常好。在这两种情况下(获得T的头部并将其移动到rev(H)的头部),我只对列表的头部感兴趣,这是非常有效的访问。

当然如果T为空,则rev(H)= rev(L)。这就是答案!

将此写为递归过程。

NODE *rev(NODE *t, NODE *rev_h) {
  if (t) {                // if t has some elements
    NODE *tail = t->next;   // save the tail of T
    t->next = rev_h;        // prepend the head to rev(H)
    return rev(tail, t);    // recur to solve the rest of the problem
  }
  return rev_h; // otherwise T is empty, so the answer is rev(H)
}

一开始,我们对rev(H)一无所知,所以T就是整个列表:

NODE *reversed_list = rev(list, NULL);

接下来要注意的是这个函数是尾递归的:递归调用是在函数返回之前执行的。这很好!这意味着我们可以轻松地将其重写为循环:

NODE *rev(NODE *t, NODE *rev_h) {
recur:
  if (t) {                // if t has some elements
    NODE *tail = t->next;   // save the tail of T
    t->next = rev_h;        // prepend the head to rev(H)
    rev_h = t;              // "simulate" the recursive call
    t = tail;               //   by setting both args
    goto recur;             //   and going back to the start
  }
  return rev_h; // otherwise T is empty, so the answer is rev(H)
}

您始终可以使用尾递归调用进行此转换。你应该认真思考为什么会有效。

现在goto很容易被重写为while循环,我们可以将rev_h初始化为NULL的局部变量,因为这是所有初始调用的确实:

NODE *rev(NODE *t) {
  NODE *rev_h = NULL;
  while (t) {             // while t has some elements
    NODE *tail = t->next;   // save the tail of T
    t->next = rev_h;        // prepend the head to rev(H)
    rev_h = t;              // "simulate" the recursive call
    t = tail;               //   by setting both args
  }
  return rev_h; // otherwise T is empty, so the answer is rev(H)
}

就地链表反向器,只需要很小的空间!

看!我们从来不必绘制有趣的方框图和箭头图或思考指针。通过仔细推理如何将问题细分为更小的自身实例,即递归的本质,它“刚刚发生”。这也是一种很好的方式,可以看到循环只是一种特殊的递归。很酷,没有?

答案 2 :(得分:1)

我假设您有类似于.c文件中预定义的以下内容

typedef struct node node_t;
struct node {
    int some_data;
    node_t *next;
};

/* Your linked list here */
typedef struct {
    node_t *head;
    node_t *foot; /* to keep track of the last element */
} list_t;

在你的功能中,你犯了一些错误

  • 不提供任何输入参数
  • 访问头 - >接下来,当程序不知道在哪里找到头

因此,导致C分段错误中最令人沮丧的错误!

相反,您应该尝试以下方法:

void reverse(list_t *mylinkedlist){
    if (mylinkedlist->head->next == NULL) {
        return;
    }
    /* do something */
}