使用指针操作链表

使用一级指针操作链表时,需要区分头结点和其他节点。特别是在创建、注销、删除时特别考虑二者的不同。Linus本人非常不推荐这种写法,这里参考以下博客重新整理而成。
http://blog.csdn.net/jasonchen_gbd/article/details/45276629
https://coolshell.cn/articles/8990.html
http://blog.csdn.net/u012234115/article/details/39717215
特此感谢。

一、一级指针操作链表的“低俗”

Linus举了一个指针的例子,解释了什么才是core low-level coding。

For example, I’ve seen too many people who delete a singly-linked list entry by keeping track of the “prev” entry, and then to delete the entry, doing something like。(例如,我见过很多人在删除一个单项链表的时候,维护了一个”prev”表项指针,然后删除当前表项,就像这样)

    /* 如果待删除的节点是链表头 */
    if (curr == head) {
        head = head->next;
    }
    else {
        prev->next = curr->next;
    }

and whenever I see code like that, I just go “This person doesn’t understand pointers”. And it’s sadly quite common.(当我看到这样的代码时,我就会想“这个人不了解指针”。令人难过的是这太常见了。)

二、核心代码分析。

正如Linus所说,既然这种方法这么不好,那么我们就来看一下这种写法。(华为OJ上有一道题大部分答案也采用的这种写法)。

/* 删除第一个value=val的链表节点 */
void deleteNode(int val)
{
    ListNode *curr = head;
    ListNode *prev = NULL; 

    while (curr != NULL)
    {
        if (curr->value == val)
        {
            /* 如果待删除的节点是链表头 */
            if (curr == head)
            {
                head = head->next;
            }
            else
            {
                prev->next = curr->next;
            }
            free(curr);
            return;
        }
        //存储previous指针并遍历所有节点
        prev = curr;
        curr = curr->next;
    }
}

如果我们需要写一个deleteNode的函数,也就是传入一个单向链表,删除一个特定的节点。
这段代码维护了两个节点指针prev和curr,标准的教科书写法——删除当前结点时,需要一个previous的指针,并且还要这里还需要做一个边界条件的判断——curr是否为链表头。于是,要删除一个节点(不是表头),只要将前一个节点的next指向当前节点的next指向的对象,即下一个节点(即:prev->next = curr->next),然后释放当前节点。
但在Linus看来,这是不懂指针的人的做法。

三、代码示例

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

//s一般表示新生节点,p表示扫描指针

/* 结构体定义 */
struct ListNode
{
    int value;
    ListNode *next;
};

/* 链表头 */
ListNode* head = NULL;

/*尾插法插入值*/
void insertNode(int val)
{
    if (head == NULL)
    {
        ListNode *s = (ListNode*)malloc(sizeof(ListNode));
        s->value = val;
        s->next = NULL;

        head = s;
        return;
    }
    else
    {
        //查找尾结点
        ListNode *p = head;
        while (p->next != NULL)
        {
            p = p->next;
        }

        ListNode *s = (ListNode*)malloc(sizeof(ListNode));
        s->value = val;
        s->next = NULL;
        //插入新生节点
        s->next = p->next;
        p->next = s;
    }
}
/* 删除第一个value=val的链表节点 */
void deleteNode(int val)
{
    ListNode *curr = head;
    ListNode *prev = NULL; 

    while (curr != NULL)
    {
        if (curr->value == val)
        {
            /* 如果待删除的节点是链表头 */
            if (curr == head)
            {
                head = head->next;
            }
            else
            {
                prev->next = curr->next;
            }
            free(curr);
            return;
        }
        //存储previous指针并遍历所有节点
        prev = curr;
        curr = curr->next;
    }
}
/*根据头结点打印(不需要修改头结点地址,因此一级指针)*/
void printNode(ListNode *myhead)
{
    if (myhead == NULL) return;
    while (myhead)
    {
        printf("%d ", myhead->value);
        myhead = myhead->next;
    }
    printf("--\n");
}

int main()
{
    //插入
    for (int i = 0; i < 3; ++i)
    {
        insertNode(i);
    }
    //打印
    printNode(head);
    //删除
    deleteNode(2);
    printNode(head);
    deleteNode(1);
    printNode(head);
    deleteNode(0);
    printNode(head);
    return 0;
}

运行效果:
这里写图片描述

四、使用二级指针

在Linus看来,这是不懂指针的人的做法。那么,什么是core low-level coding呢?那就是有效地利用二级指针,将其作为管理和操作链表的首要选项。代码如下:

/* 删除value=val的链表节点 */
int delete_from_list(int val)
{
    ListNode ** curr = &head; //正在遍历的节点的指针  
    ListNode * entry; //正在遍历的节点  

    while (*curr)
    {
        entry = *curr; //注2
        if (entry->value == val)
        {
            /* 删除entry节点 */
            *curr = entry->next; //注3
            free(entry);
            return 0;
        }
        /* 遍历所有节点的指针 */
        curr = &(entry->next); //注1

    }
    return -1;
}

使用二级指针,我们可以把头结点和其他节点等效对待。
这里写图片描述

同上一段代码有何改进呢?我们看到:不需要prev指针了,也不需要再去判断是否为链表头了,但是,curr变成了一个指向指针的指针。这正是这段程序的精妙之处。(注意,我所highlight的那三行代码)

让我们来人肉跑一下这个代码,对于——

删除节点是表头的情况,输入参数中传入head的二级指针,在for循环里将其初始化curr,然后entry就是*head(*curr),我们马上删除它,那么第8行就等效于*head = (*head)->next,就是删除表头的实现。
删除节点不是表头的情况,对于上面的代码,我们可以看到——
1)注1:如果不删除当前结点 —— curr保存的是当前结点next指针的地址。

2)注2: entry 保存了 *curr —— 这意味着在下一次循环:entry就是prev->next指针所指向的内存。

3)注3:删除结点:*curr = entry->next; —— 于是:prev->next 指向了 entry -> next;

猜你喜欢

转载自blog.csdn.net/u013457167/article/details/79623821