线性表之单向循环链表

客官先看看笔者的实现

这篇文章来讲述线性表中的单向循环链表,完整的实现请自行导航到gitee托管仓库。
传送门:https://gitee.com/prexer/data-structure-and-algorithms
所有的样例,都可以通过如下方式进行编译和检测:

$ git clone https://gitee.com/prexer/data-structure-and-algorithms.git
$ cd data-structure-and-algorithms/liner_list/single_circular_linked_list
$ make run

循环列表是否需要重新抽象?

答案看起来是否定的,因为循环列表和非循环列表的数据抽象是一样的,所以我们不需要重新来抽象数据类型的属性,依然如下:

typedef int ItemType;
typedef struct NODE {
    struct Node * next;
    ItemType value;
} Node;

没错,数据类型的属性并没有变化,但循环列表的特殊,就在于操作它的方式,确切的说一切都始于插入操作,它让链表的结尾不再是NULL,而是表结构的开始元素。

填在给我们的实现添加约束:

  1. 表结构是有序的,从小到大排列
  2. 表结构没有表头节点,仅仅有一个记录进入表的指针,任何一个指向表节点的指针,都可以作为头指针
  3. 表中的元素允许重复

看起来是不是很简单,其实实现起来还是需要点功底,不信你自己试试?

实现单向循环列表的插入操作

同样的,我们并不给出完成的程序结构,而是将插入操作的实现放在文章中分析,套路依旧是:

  1. 用自己的语言描述出来你要实现的内容
  2. 看看笔者的笨拙实现
  3. 小小的分析和总结

如何描述单向循环列表的插入操作?

脑袋里有东西后,需要让它能够用自己的语言说出来,这样学习才会高效。

对于插入操作,我们知道,由于程序会在插入操作的时候修改头指针,指针的指针必不可少。
那么函数原型应该描述为:
插入操作接收一个指向头指针的指针和一个待插入的新值,并返回表示操作是否成功的布尔值。

在实现代码之前,笔者总是会在纸上或者脑袋里进行分析和演算,这样做有很大的帮助,其实计算机行业真正具有价值的部分是计算思维,而不是你动手敲代码的快感,如果你能保证自己写的代码经过很少的调试就能完全符合需求,那你就是天才。而像我这样的普通人,一定要有腹稿才行,想好了再写才是快速的方法。

顺便说一句,写代码,敲击键盘只是体力劳动,而程序员的工作应该是脑力劳动才对。

插入操作最难的地方就是你要知道插入的位置是否特殊,所以我们分为:

  • 插入的位置在头部(这里说头部并不准确,应该说是头指针指向的那个节点,毕竟我们说了,头指针可以换)
  • 插入的位置在非头部,也就是其他的位置,哪怕表中只有一个节点,依旧符合这个规律

在实现之前,你要知道为什么这么区分?
因为我们是循环链表,一旦头部节点变化的时候,指向它的节点都要变化,而指向它的节点有两个:

  1. 一个是头指针
  2. 另一个是尾巴节点的next指针

掌握了这个,赶快取实现吧!

实现的代码

static Node ** get_tail(Node ** linkp){
    Node * current = *linkp;
    Node * head = *linkp;
    int counter = 0;
    if (current != NULL){
        while (! (current == head && counter != 0)){
                linkp = &(current->next);
                current = *linkp;
                counter++;
        }
        return linkp;
    }else{
        return NULL;
    }
}

int scll_insert(Node ** linkp, ItemType new_value){
    
    Node * current = *linkp;
    Node * head = *linkp;
    Node ** orig = linkp;
    int counter = 0;

    if (current != NULL){
        while (! (current == head && counter != 0) ){
            if (current->value >= new_value){
                break;
            }
            else {
                linkp = &(current->next);
                current = *linkp;
                counter++;
            }
        }
    }

    Node * new = malloc(sizeof(Node));
    if (new == NULL)
        return ERROR;
    new->value = new_value; 

    if (current == NULL){
        new->next = new;
        *linkp = new;
    }else{
        if (current == head && counter == 0){
            Node ** tail_of_next = get_tail(orig);
            new->next = current;
            *orig = new;
            *tail_of_next = new;
        }else{
            new->next = current;
            *linkp = new;
        }
    }

    return OK;
}

让你的脑袋动起来

写了这么多代码,其实真正能剩下的,基本都是你总结过的,并且面向一定问题的记忆,这种东西人们称为经验;而往往这种东西是没有办法快速获得的,都要经过思考和沉淀才可以获得这些记忆印记。

那我们就开始思考这个实现,然后想想有什么可以总结和归纳的。

首先,就像笔者之前提及的那样,C语言中的函数调用都是值传递,所以,函数内部的形参变量你随便该它本身,并没有影响;同样的套路也适合于在函数中定义的临时变量,通常他们被申请在栈空间上,你就算修改了他们也不会对外界有影响,这也是函数式编程常用的技巧。

由于我们需要一个可以随着迭代前进的变量,所以定义了一个current,你可以随便修改它。

由于我们没有头节点,只有一个头指针,所以我们定义了定义一个记录头部的位置指针,这样绕一圈,回来的时候,你知道在哪里可以停止,这就是head的用法。

orig用来在第一个节点替换或者修改的时候,来修改头指针的,这也是为什么它是指针的指针。

还有一个重要的变量是counter,这是一个技巧,用来判断遍历是否进行的标志,如果没有进行它就是0

定义好变量后,就是要注意的细节

  1. 指针一定要在解引用之前判断是否是NULL,这是常识,这也是判断表是否为空的标志
  2. while (! (current == head && counter != 0) )这个语句值得考究,首先笔者是这样思考的
    • 首先我们判断什么时候应该结束,也是正常思考的方向,停止条件其实有两个,一个是绕一圈回来了,另一个是找到了匹配的位置
    • 而这里要注意,这量个条件是有先后顺序的,先判断什么,后判断什么是很重要的,所以if (current->value >= new_value)在while内部
  3. 一定要记得让自己的程序趋近于终止条件,如果没有就回去检查,你可以看看我们程序中是如何趋近终止的

处理完这部分之后,重要的部分就是待插入节点应该插入的位置,其实他有三种情况:

  1. 空表插入
  2. 在表的第一个节点之前插入
  3. 在表的非第一个节点之前插入

这里推荐一个好的思考方法:

  1. 用笔在纸上画出简单的链表链接图,一定要注意标注指针和箭头
  2. 对于插入操作来说,有两个步骤,一个是插入之前的图示,一个是插入之后的图示,一定要画出来
  3. 对于插入后的图中,你要修改的规则是
    1. 把new指向的部分先在程序中实现(这里先后顺序很重要)
    2. 把指向new的节点按照不会被覆盖的顺序修改
    3. 再数一遍箭头,看是否有漏掉

完成这些,基本上你的程序就不会错。

再次提醒,这里有一个函数是get_tail获得指向尾节点next指针的指针,函数中都是用的临时变量和实参拷贝,所以可以随便修改;而且第一个节点切换的时候,尾巴一定要跟着修改

其他相关的操作实现细节解释

scll_locate依旧是需要:记录第一个节点的head和counter计数器,然后定义一个推进的变量就完成了。

scll_delete是需要注意一点,它分为三种情况:(这里已经判断过表为空的情况,下面不考虑这个)

  1. 如果表中只有一个节点,并删除这个唯一的节点,这个节点被删除后,需要把*rootp设置为NULL,一定要注意,环型链表,只有一个节点的时候,自己的next会指向自己,这样*rootp = record->next就无法工作了,所以不能和下一个条件写在一起
  2. 如果删除的是一个并非只有一个节点的列表中第一个节点
  3. 如果删除的是一个并非只有一个节点的列表中非第一个节点

比较投机取巧的实现是下面两个函数

  1. scll_modify: 它利用了scll_delete和scll_insert来完成处理
  2. scll_destroy: 则是利用了scll_delete来逐个删除

好了,就是这些看似简单的东西,需要注意的还是蛮多的,最后多说一句,一定要画图哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值