资讯详情

万字篇幅详解C++实现链表类数据结构以及反转链表方法

0.导读

本文记录了学习链表数据结构并试图模仿它的衣陈int本markdown用原码存储衣服gitee仓库(文件名intlist.cpp),建议这篇文章和原码一起吃,希望能激励你。

//是复制的markdown文本,代码块不是很好Q.Q

本文包括:实现链表类的所有知识、思想、心路历程、疑惑和答案。

  • 链表概念

  • 成员属性、方法和选择链表类别的原因。

  • 链表的push_back与头结点新建问题

  • insert 与 earse实现及其边界:头插尾插头删尾

  • 基于递归的反转链表练笔日志

核心素养:

  • 数据结构思想

  • 边界分析思想

  • 变量作用域和值地址的测量

1.链表概念

这里建议观看翁恺的C语言课堂介绍,这是个人遇到的解释链表最清晰的视频。

我们不应该开始实战。

核心思想是:为什么我们需要链表?因为链表的插入和删除比数组更有效。

需要关注的是链表和动态数组数据存储思想的分析。

这里一图胜千言

2.为了实现链表类别,我们需要设计:

  • 包装所有成员属性和方法的链表类

  • 成员属性结点类是链表的单元

  • 头结点,是链表的起点。

  • 长度记录器方便 我们的增删改查。

  • 尾插操作的起点是尾插操作的起点

class intList { public:     class node {     public:         node() {             this->data = 0;             this->next = NULL;         }         /*node(int elem) {             this->data = elem;             this->next = NULL;         }*////没有使用参结构,因为我们维护节点数据类型的指针         int data;         node* next;     };     node* head;     int len; };

思考:指针域指向的对象是下一节点的数据类型还是下一节点存储的数据?

如何实现头结点?

我们需要的是节点的实例对象还是它的指针?

答案是指针。维护 , 维护

如果实现了动态数组,我们应该知道维护方法是指针

我们将head和next设计为node的指针,cout<\<head->next->next->next->data;可以实现这种嵌套寻址。

head.next=1st;

思考:尾结点的声明时机?(变量作用域的衡量)

保留此问。

3.新链表和尾插的问题

        intList() {         this->head = new node;         this->head->next = NULL;         this->len = -1;             }     void push_back(int elem) {         if (this->head->next==NULL) {             //因为链表类可以先创建,再删除,在push。             //链表的初始化:指针分配空间后写入数据NULL。                 this->head = new node;      //需要注意的是,头结点应该在无参结构中初始化,否则,报野指针                 this->head->next = NULL;                 this->len = -1;                          //相反,创建指针对象并将其写入匿名数据                 node* pos = new node;                 pos->data = elem;                 this->head->next = pos;                 this->len  ;         //  cout << this->head->next->data<<" ";         }     ///正常尾插,查找方法searchNo后来解释了方法     else {             //寻尾             node* pos = this->searchNo(this->len);             node* come = new node;             come->data = elem;             pos->next = come;             pos = come;             this->len  ;         //  cout << this->pos->data << " ";         }     }

首先是链表的初始化时机。起初,我写在类别的无参与结构中,但在实战中,我发现它是先建立的,然后删除为空,然后删除为空pushback因此,改为每次pushback判断是否在新建链表。

Q:一大难点是为什么要在这里使用new操作呢?

A:因为head初始为空指针,而我们想对一个空指针进行操作的话,应先为指针解引的地址分配空间。

所以head是new为了保持风格一致,后面pos和come(见下文)也应为指针类型。

这样,对于单元节点,我们节点的指针,并使用堆区new操作维护节点中的数据。

优雅。这也与动态二维数组的思想交换

我们比较以下代码

                node pos;                 pos.data=elem;                 this->head->next = &pos;

Q:为了连接链表,我们使用了一个选址操作,所有的尾插都使用了这种风格的新节点,不妨考虑一下this->head最后指向什么? ?(涉及上文结尾声明的时机)

Q:是否可以强求这样node pos?

A: 是的,我们把node pos;设置为list在push_back引用其他涉及结束点更新的方法转换引用传入传出pos。

,翁恺老师说全局变量是有害的,为了强制设计实例对象,增强类中的耦合度是不合理的。

无论如何,这样的操作都不够优雅,next指向下一个节点实例对象的地址吗?

不是的,next指向下一个节点类指针,即指针和指针的赋值操作。

总结:这其实是一个变量作用域和值与地址的问题,而且这个问题藏得很深,很难发现会像作者一样陷入纠结,没有纠结到点,通过跟踪程序很难发现。作者在梳理这篇文章markdown当我发现这个问题时,我真的回答了为什么head,pos一定是指针。

同时回答以上,pos应该是局部变量,用new尾插时声明风格。

4.遍历结构

这里直接从翁恺老师的视频中学习控制结构

for (node* it = this->head->next; it; it = it->next)

关于真假: c 中0是假的,非0是真的,我们可以尝试输出空指针,结果是0x这是十六进制位模式的0000000。

优雅。循环条件是it为真(即指针有指向,不是空指针),迭代句子it自身更新。

4.1遍历输出

void ptfme() {
        for (node* it = this->head->next; it; it = it->next)
        {
            cout << it->data << " ";
        }
    }

4.2遍历查找

对于链表由于它的非连续性,我们是不可能像数组那样及其简单地获得某某节点的地址的。

解决方法为,记录一次遍历中(它的功能不正是如此吗)的更新次数,直到这个次数等于我们的预期,返回这个节点(指针)

即,searchNo(int no)方法,得到No.x号元素的地址。

node* searchNo(int no)
    {
        int ans = 0; int is_found = 1;
        for (node* it = this->head->next; it; it = it->next, ans++)
        {
            if (ans == no)
            {
                is_found = 0;
        //      cout << "found\n";
                return it;
            }
        }
        if (!is_found) {
        //  cout << "no this No.x\n";
            return NULL;
        }
    }

基于这个方法,不就可以得到容器的迭代器了吗

4.3基于查找的插删操作

现在我们可以轻松获得链表中任意位置的对象指针了,(溢出部分的代价由操作者承担),插删操作就有了起点

首先是插删的基本参数,位置和插入对象。

然后是链表插删的思想:断链,重新成链。

//这里仅实现中间插入:用serachNo方法获得插入点左右节点,插入come的左侧comel.next改为链接come,come。next链接come右侧come1;
//事实上,当情况为头插尾插时,又有不同。应该加上if语句判断边界处理
void insert(int no,int elem) {
        node* comel = this->searchNo(no);
        node* comer = this->searchNo(no)->next;
        node* come = new node;
        come->data = elem;
        comel->next = come;
        come->next = comer;
        this->len++;
        cout << "is_insert,new inlist:\n";
    
        this->ptfme();
    }
//这里先实现普通删除,用searchNo方法获得删除节点,左侧链接右侧,重点delete掉。
    void earse(int no) {
        if (no == this->len)
        {
            this->pop_back();
        }
        else 
        {   if (no == 0)
            this->pop_head(); 
            else
            {
            node* earsing = this->searchNo(no);
            node* earl = this->searchNo(no - 1);
            node* earr = this->searchNo(no)->next;
            earl->next = earr;
            delete earsing;
            earsing = NULL;
            this->len--;
            cout << "is_earse,newlint:\n";
            this->ptfme();
            }
        }

4.4插删操作的边界:头尾插删

尾插已实现。

以下给出头尾删

void pop_back() {
        //一般尾删需要找到倒数第二个元素即链表长度大于等于二
        //获得尾结点指针,删除尾结点内容,更新尾结点为倒二节点。
        if (this->head->next->next)
        {
            node* pos = this->searchNo(this->len);
            node* newpos = this->searchNo(--this->len);
            //  cout << "popelem:" << newpos->next->data;
            delete newpos->next;
            newpos->next = NULL;
            pos = newpos;
            //if(newpos->next)cout << "popelem:" << newpos->next->data;
            this->ptfme();
            return;
        }
        else
        {
            //一个元素的尾删,即链表置空
            //获得节点指针,delete,头结点置空
            if (this->head->next)
            {
                node* temp = this->searchNo(0);
                delete temp;
                temp = NULL;
                this->head->next = NULL;
                this->ptfme();
                return;
            }
            else {
                cout << "NULL_inilist,wrong_pop";
                return;
            }
        }
​
    }
//头删同理
    void pop_head() {
        //2+
        if (this->head->next->next)
        {
            node* newhead = new node;
            newhead->next = this->searchNo(1);
            delete this->head;
            this->head = newhead;
            cout << "is head_back,newlist:\n";
            this->ptfme();
        }
        else
        {
            if (this->head->next)
            {
                node* temp = this->searchNo(0);
                delete temp;
                temp = NULL;
                this->head->next = NULL;
                this->ptfme();
                return;
            }
            else {
                cout << "NULL_inilist,wrong_pop";
                return;
            }
        }
    }

头插略(忘了写QAQ)

思想都是想通的,当做您的小练笔吧(:> ,<:)

5.基于递归实现反转链表

是力扣挺经典的一道题,我就顺带拿来练笔了

不同于算法题只需要一种返回翻转完毕后的链表的头结点的算法,我希望的是获得一种把this对象翻转的方法

所以最终我设计了如下框架

//对于实节点数少于两个(0或1)的链表,做空实现,对于合法的链表,做算法得到newhead,用newhead更新this->head
void reverse()
    {
        if (this->head->next->next)
        {
            node* newhead= _reverse(this->head);
            this->head= newhead;
        }
            
        this->ptfme();//打印列表函数
        return;
​
    }
node* _reverse(node* _head){}

解决完这些前置的小问题,来到重难点递归算法部分

推荐观看@bilibili_筱可爱233 的算法讲解视频

先上算法

//蛮短但蛮难
    node* _reverse(node* _head) {
        //递归结构
        if (_head->next==NULL) return _head;
        node* newhead = _reverse(_head->next); //
                    //反转算法
                    _head->next->next = _head;
                    _head->next = NULL;
​
        return newhead;//   
    }

如果你和笔者一样初识递归或者一直难以搞定递归,建议多上草稿本,画画递归闭环和出口,写写程序运行流程,以下是笔者对这个算法的个人理解,仅供参考。

如上分隔,这个算法分为两部分,且看我一图讲完

第一部分为递归结构,用以进入最底层递归和获取尾节点指针

经过如图,经紫框的控制结构,我们获得最底层递归的result 5th,退出5th递归据需4th递归,做了与value第二部分的算法后返回value,退出4th进入3rd,……最终把value()传递给target(intlist::node* newhead)

第二部分为算法内容,用以修改链表指向。

赋值语句是要从右往左看的,不难看出算法的起点是倒数第二层递归,算法主体head是func4的参(4th节点),所以尾结点表达式为head.next

所以是吧head.next左边的指针的指向(指向head.next)赋给了head.next的右边。

这样head.next.next不就指向了它左边的节点吗。

注意这里的head.next.next并不是6th节点,我们并没有为他new空间也没有声明6th节点,它的正确读法应该为(head.next).next,

这里的next并非时如它的标识符那样指下一个节点,而是对象指针(head->next)的解引的成员属性——它的指针域,现在指向了4th。

做完这些后,4th递归结束,返回与上述算法无关的新链表(is_building)的new_head,然后返回值传给了func3的value,依次上传。而fun3的参是3th节点指针,重复上述算法对(3th.next)这个节点指针修改,结果是4th的next(在上一层递归中被擦除)被修改为了3th,3th.next被擦除。依次进行算法,新链表最终建立完成。

小结,核心素养是对“next”这个概念的辨析,他只是一个指针而不是“下一个”的意思,可以说是很有误导性了。

结果:

原因很显然啊,我们的算法起点是4th,结束点就并非是1st而是原来的空头结点(默认值为0),所以会带0;又因为我们处理这个算法返回的头结点的语句是this->head= newhead;而在我们的遍历结构中起点是this.head.next;所以会发生“左截断,右延长”的灵异现象

思考:上述语句改为this->head->next= newhead;呢?

结果:

很有意思,确实成功包含了10th元素 9,也有完整的9到1的翻转列表。却陷入了死循环

因为没有管理head的指向

解决方法:将this->head new一下

            node* newhead= _reverse(this->head);
            this->head = new node;
            this->head->next= newhead;

思考,覆盖指针head而并未delete回收head,堆区数据是否丢失:否,经我们的操作head又重新链回了这段数据。

输出结果显然是“9 8 7 6 5 4 3 2 1 0”

尾删也不太对,因为“0”是head指针来的,这个head指针被是尾结点.next连上,形成闭环链表,而尾删方法作用对象是尾结点。所以我们要删除尾结点.next

void reverse()
    {
        if (this->head->next->next)
        {
            node* newhead= _reverse(this->head);
            this->head->next= newhead;
        }
    //错误的,尾删
        this.pop_back();
        this->ptfme();//打印列表函数
        return;
​
    }

//正确的,尾.next删
        node* tail = this->searchNo(this->len);
        tail->next = NULL;

结果符合预期

至此,基于递归的翻转链表实现完成,特别有意思真的,自己捣鼓试出了好多东西,可能这就是计算机的魅力吧。

Q:最后的最后是链表类的析构问题,如何回收我们再堆区写入的每个节点的数据?

A: 必须分清楚delete的目标,删除我们声明的每个与堆区有关的标识符吗?

不是的,是写在堆区那块物理空间的内的数据。我们仅需要一种算法,得到每个节点的地址(即通过searchNo方法得到这个节点的指针),delete指针即可。

6.析构问题:delete节点

核心问题是要free掉节点的同时不丢失next指针,所以,先创建q维护p.next节点,再delete掉p,以下为《大话数据结构》中的实现

后记:此次对链表容器的实现,较好地锻炼了数据结构思维与边界处理思维,变量选取的权衡。

数据结构与与算法真的真的太核心了,我是用c++实现的嘛,可是在参阅资料时那些基于java,c的课程其实都能听的,因为底层的数据结构和算法都是共通的(其中感触最深的莫过于翁凯的c语言给出的链表遍历结构)

不妨将我们的cpp拆分为头文件和cpp,就获得了专属的int类型list容器。

最后贴上成品原码(懒得git的话)

#include<bits/stdc++.h>
using namespace std;
class intList {
public:
    class node {
    public:
        node() {
            this->data = 0;
            this->next = NULL;
        }
        /*node(int elem) {
            this->data = elem;
            this->next = NULL;
        }*///并没有用到有参构造,因为我们维护的是节点数据类型的指针
        int data;
        node* next;
    };
    node* head;
    int len;
            intList() {
                this->head = new node;
                this->head->next = NULL;
                this->len = -1;
                    }
    void push_back(int elem) {
        if (this->head->next==NULL) {
            //结点的新建应该包含pos的初始化,因为链表类可以先被创建,再删除干净,在push。
​
                this->head = new node;      
                this->head->next = NULL;
                this->len = -1;
            
            //不是深拷贝,而是创建指针对象并写入匿名数据
                node* pos = new node;
                pos->data = elem;
                this->head->next = pos;
                this->len++;
        //  cout << this->head->next->data<<" ";
        }
        else {
            //寻尾
            node* pos = this->searchNo(this->len);
            node* come = new node;
            come->data = elem;
            pos->next = come;
            pos = come;
            this->len++;
        //  cout << this->pos->data << " ";
        }
    }
    //遍历
    void ptfme() {
        cout << endl;
        for (node* it = this->head->next; it; it = it->next)
        {
            cout << it->data << " ";
        }
        cout << "\nptfover\n";
        return;
    }   
    
    node* searchNo(int no)
    {
        int ans = 0; int is_found = 1;
        for (node* it = this->head->next; it; it = it->next, ans++)
        {
            if (ans == no)
            {
                is_found = 0;
        //      cout << "found\n";
                return it;
            }
        }
        if (!is_found) {
        //  cout << "no this no\n";
            return NULL;
        }
    }
    void ptffrom(int no) {
        for (node* it=this->searchNo(no); it; it = it->next)
        {
            cout << it->data << " ";
        }
    }
    void insert(int no,int elem) {
        node* comel = this->searchNo(no);
        node* comer = this->searchNo(no)->next;
        node* come = new node;
        come->data = elem;
        comel->next = come;
        come->next = comer;
        this->len++;
        cout << "is_insert,new inlist:\n";
    
        this->ptfme();
    }
    void earse(int no) {
        if (no == this->len)
        {
            this->pop_back();
        }
        else 
        {   if (no == 0)
            this->pop_head(); 
            else
            {
            node* earsing = this->searchNo(no);
            node* earl = this->searchNo(no - 1);
            node* earr = this->searchNo(no)->next;
            earl->next = earr;
            delete earsing;
            earsing = NULL;
            this->len--;
            cout << "is_earse,newlint:\n";
            this->ptfme();
            }
        }
        
        
    }
    //头尾插删有边界问题
    void pop_back() {
        //一般尾删需要找到倒数第二个元素即链表长度大于等于二
        if (this->head->next->next)
        {
            node* pos = this->searchNo(this->len);
            node* newpos = this->searchNo(--this->len);
            //  cout << "popelem:" << newpos->next->data;
            delete newpos->next;
            newpos->next = NULL;
            pos = newpos;
            //if(newpos->next)cout << "popelem:" << newpos->next->data;
            this->ptfme();
            return;
        }
        else
        {
            //一个元素的尾删
            if (this->head->next)
            {
                node* temp = this->searchNo(0);
                delete temp;
                temp = NULL;
                this->head->next = NULL;
                this->ptfme();
                return;
            }
            else {
                cout << "NULL_inilist,wrong_pop";
                return;
            }
        }
​
    }
    void pop_head() {
        //2+
        if (this->head->next->next)
        {
            node* newhead = new node;
            newhead->next = this->searchNo(1);
            delete this->head;
            this->head = newhead;
            cout << "is head_back,newlist:\n";
            this->ptfme();
        }
        else
        {
            if (this->head->next)
            {
                node* temp = this->searchNo(0);
                delete temp;
                temp = NULL;
                this->head->next = NULL;
                this->ptfme();
                return;
            }
            else {
                cout << "NULL_inilist,wrong_pop";
                return;
            }
        }
    }
    //递归结构实现链表翻转 返回新链表头结点
    //我们这种头结点结构不太适用,因为
    void reverse()
    {
        if (this->head->next->next)
        {
            node* newhead= _reverse(this->head);
            this->head = new node;
            this->head->next= newhead;
        }
        //this->pop_back();
        //正确的尾.next删
        node* tail = this->searchNo(this->len);
        tail->next = NULL;
        this->ptfme();
        return;
​
    }
    node* _reverse(node* _head) {
        if (_head->next==NULL) return _head;
        node* newhead = _reverse(_head->next); //
​
        _head->next->next = _head;
        _head->next = NULL;
​
        return newhead;//   
    }
​
};
int main() {
    intList intlist;
    //intlist.push_back(1);
//  intlist.push_back(0);
//  intlist.pop_head(); 
    for (int i = 1; i < 10; i++)intlist.push_back(i);
    intlist.ptfme();
    intlist.reverse();
    intlist.reverse();
    return 0;
​
    //intlist.earse(intlist.len);
    
    //cout << intlist.head->next->data << " " << intlist.head->next->next->data << " "; 结点链接测试案例
    //cout << intlist.pos->data;尾结点测试案例
    //intlist.ptfme();
}

标签: 7jb4继电器3th2040

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司