数据结构之链表的实现

链表分为单链表、双链表、循环链表

先看最简单的单链表

参考:【王道考研】王道数据结构与算法详细笔记(全)_王道数据结构笔记-CSDN博客

单链表的实现

单链表:用链式存储实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。

特点:

优点:不要求大片连续空间,改变容量方便。

缺点:不可随机存取,要耗费一定空间存放指针。

两种实现方式:

带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。

不带头结点,麻烦。对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。

定义单链表

typedef struct LNode
{                      //定义单链表结点类型
    ElemType data;     //数据域
    struct LNode *next;//指针域
}LNode, *LinkList;

注意,链表定义时是定义结点。然后需要链表时定义一个结点指针,表示一个链表的开始,也就可以用来表示链表了。

不带头结点初始化

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
//初始化一个空的单链表
bool InitList(LinkList &L){
    L = NULL; //空表,暂时还没有任何结点
    return true;
}
 
void test(){
    LinkList L;  //声明一个指向单链表的头指针
    //初始化一个空表
    InitList(L);
    ...
}
 
//判断单链表是否为空
bool Empty(LinkList L){
    return (L==NULL)
}

带头结点初始化

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{  
    L = (LNode*) malloc(sizeof(LNode));  //头指针指向的结点——分配一个头结点(不存储数据)
    if (L == NULL)          //内存不足,分配失败
        return false;
    L -> next = NULL;       //头结点之后暂时还没有结点
    return true;
}
 
void test()
{
    LinkList L;  //声明一个指向单链表的指针: 头指针
    //初始化一个空表
    InitList(L);
    //...
}
 
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
    if (L->next == NULL)
        return true;
    else
        return false;
}

头指针其实就是定义一个链表结点结构体的指针,用来表示一个链表,一般会指向头结点。

按位序插入(带头结点) Listlnsert(&Li,e): 插入操作。在表L中的第i个位置上插入指定元素e 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。 平均时间复杂度:O(n)

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e)
{  
    //判断i的合法性, i是位序号(从1开始)
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)
 
    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }
 
    if (p==NULL)                 //如果p指针知道最后再往后就是NULL
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;                 //将结点s连到p后,后两步千万不能颠倒qwq
 
    return true;
}

按位序插入(不带头结点) Listlnsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
bool ListInsert(LinkList &L, int i, ElemType e)
{
    if(i<1)
        return false;
    
    //插入到第1个位置时的操作有所不同!
    if(i==1){
        LNode *s = (LNode *)malloc(size of(LNode));
        s->data =e;
        s->next =L;
        L=s;          //头指针指向新结点
        return true;
    }
 
    //i>1的情况与带头结点一样!唯一区别是j的初始值为1
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=1;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)
 
    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }
 
    if (p==NULL)                 //i值不合法
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;          
    return true;
 
}

指定结点的后插操作 InsertNextNode(LNode *p, ElemType e); 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
bool InsertNextNode(LNode *p, ElemType e)
{
    if(p==NULL){
        return false;
    }
 
    LNode *s = (LNode *)malloc(sizeof(LNode));
    //某些情况下分配失败,比如内存不足
    if(s==NULL)
        return false;
    s->data = e;          //用结点s保存数据元素e 
    s->next = p->next;
    p->next = s;          //将结点s连到p之后
 
    return true;
}                         //平均时间复杂度 = O(1)
 
 
//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e)
{  
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)
 
    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后4鸟会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }
 
    return InsertNextNode(p, e)
}
 

指定结点的前插操作 设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1)

//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType e){
    if(p==NULL)
        return false;
    
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL) //内存分配失败
        return false;
 
    //重点来了!
    s->next = p->next;
    p->next = s;       //新结点s连到p之后
    s->data = p->data; //将p中元素复制到s
    p->data = e;       //p中元素覆盖为e
 
    return true;
} 

头插

LinkList List_HeadInsert(LinkList &L){       //逆向建立单链表
    LNode *s;
    int x;
    L = (LinkList)malloc(sizeof(LNode));     //建立头结点
    L->next = NULL;                          //初始为空链表,这步不能少!
 
    scanf("%d", &x);                         //输入要插入的结点的值
    while(x!=9999){                          //输入9999表结束
        s = (LNode *)malloc(sizeof(LNode));  //创建新结点
        s->data = x;
        s->next = L->next;
        L->next = s;                         //将新结点插入表中,L为头指针
        scanf("%d", &x);   
    }
    return L;
   
}
 

尾插

// 使用尾插法建立单链表L
LinkList List_TailInsert(LinkList &L){   
    int x;			//设ElemType为整型int  
    L = (LinkList)malloc(sizeof(LNode));     //建立头结点(初始化空表)     
    LNode *s, *r = L;                        //r为表尾指针    
    scanf("%d", &x);                         //输入要插入的结点的值   
    while(x!=9999){                          //输入9999表示结束     
        s = (LNode *)malloc(sizeof(LNode));    
        s->data = x;           
        r->next = s;           
        r = s;                               //r指针指向新的表尾结点     
        scanf("%d", &x);       
    }    
    r->next = NULL;                          //尾结点指针置空      
    return L;
}

参考:

浅析线性表(链表)的头插法和尾插法的区别及优缺点 - swing·wang - 博客园 (cnblogs.com)

【数据结构】:单链表之头插法和尾插法(动图+图解)_头插法和尾插法图解-CSDN博客

按位序删除节点 ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点; 思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;
 
bool ListDelete(LinkList &L, int i, ElenType &e){
    if(i<1) return false;
 
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)
 
    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){     //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }
 
    if(p==NULL) 
        return false;
    if(p->next == NULL) //第i-1个结点之后已无其他结点
        return false;
 
    LNode *q = p->next;         //令q指向被删除的结点
    e = q->data;                //用e返回被删除元素的值
    p->next = q->next;          //将*q结点从链中“断开”
    free(q)                     //释放结点的存储空间
 
    return true;
}

指定结点的删除

bool DeleteNode(LNode *p){
    if(p==NULL)
        return false;
    
    LNode *q = p->next;      //令q指向*p的后继结点
    p->data = p->next->data; //让p和后继结点交换数据域
    p->next = q->next;       //将*q结点从链中“断开”
    free(q);
    return true;
} //时间复杂度 = O(1)

单链表的按位查找 GetElem(L, i): 按位查找操作,获取表L中第i个位置的元素的值; 平均时间复杂度O(n)

LNode * GetElem(LinkList L, int i){
    if(i<0) return NULL;
    
    LNode *p;               //指针p指向当前扫描到的结点
    int j=0;                //当前p指向的是第几个结点
    p = L;                  //L指向头结点,头结点是第0个结点(不存数据)
    while(p!=NULL && j<i){  //循环找到第i个结点
        p = p->next;
        j++;
    }
 
    return p;               //返回p指针指向的值
}

单链表不具备随机访问的特性,查找时只能依次遍历。

单链表的按值查找 LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素; 平均时间复杂度:O(n)

LNode * LocateElem(LinkList L, ElemType e){
    LNode *P = L->next;    //p指向第一个结点
    //从第一个结点开始查找数据域为e的结点
    while(p!=NULL && p->data != e){
        p = p->next;
    }
    return p;           //找到后返回该结点指针,否则返回NULL
}

求单链表的长度 Length(LinkList L):计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。 算法的时间复杂度为O(n)

int Length(LinkList L){
    int len=0;       //统计表长
    LNode *p = L;
    while(p->next != NULL){
        p = p->next;
        len++;
    }
    return len;
}

更多待补充。 

双链表

其实和单链表大同小异,只不过结点中既有后指针,又有前指针。

双链表中节点类型的描述

typedef struct DNode{            //定义双链表结点类型
    ElemType data;               //数据域
    struct DNode *prior, *next;  //前驱和后继指针
}DNode, *DLinklist;
 

双链表的初始化(带头结点)

typedef struct DNode{            //定义双链表结点类型
    ElemType data;               //数据域
    struct DNode *prior, *next;  //前驱和后继指针
}DNode, *DLinklist;
 
//初始化双链表
bool InitDLinkList(Dlinklist &L){
    L = (DNode *)malloc(sizeof(DNode));      //分配一个头结点
    if(L==NULL)                              //内存不足,分配失败
        return false;
    
    L->prior = NULL;   //头结点的prior指针永远指向NULL
    L->next = NULL;    //头结点之后暂时还没有结点
    return true;
}
 
void testDLinkList(){
    //初始化双链表
    DLinklist L;         // 定义指向头结点的指针L
    InitDLinkList(L);    //申请一片空间用于存放头结点,指针L指向这个头结点
    //...
}
 
//判断双链表是否为空
bool Empty(DLinklist L){
    if(L->next == NULL)    //判断头结点的next指针是否为空
        return true;
    else
        return false;
}

双链表的插入操作 后插操作 InsertNextDNode(p, s): 在p结点后插入s结点

bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后
    if(p==NULL || s==NULL) //非法参数
        return false;
    
    s->next = p->next;
    if (p->next != NULL)   //p不是最后一个结点=p有后继结点  
        p->next->prior = s;
    s->prior = p;
    p->next = s;
    
    return true;
}

双链表的删除操作 删除p节点的后继节点

//删除p结点的后继结点
bool DeletNextDNode(DNode *p){
    if(p==NULL) return false;
    DNode *q =p->next;            //找到p的后继结点q
    if(q==NULL) return false;     //p没有后继结点;
    p->next = q->next;
    if(q->next != NULL)           //q结点不是最后一个结点
        q->next->prior=p;
    free(q);
 
    return true;
}
 
//销毁一个双链表
bool DestoryList(DLinklist &L){
    //循环释放各个数据结点
    while(L->next != NULL){
        DeletNextDNode(L);  //删除头结点的后继结点
    free(L); //释放头结点
    L=NULL;  //头指针指向NULL
 
    }
}
 

双链表的遍历操作

前向遍历

while(p!=NULL){
    //对结点p做相应处理,eg打印
    p = p->prior;
}

后向遍历

while(p!=NULL){
    //对结点p做相应处理,eg打印
    p = p->next;
}

双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)

循环链表

循环单链表 结点定义方式和单链表一样,只不过最后一个结点的指针不是NULL,而是指向头结点。

循环双链表 结点定义方式和双链表一样,只不过表头结点的prior指向表尾结点,表尾结点的next指向头结点。

暂略。

链表补充

创建一个链表,最开始不是考虑头结点的问题,没有头结点也可以,而是先考虑头指针的问题,因为头指针用来标识一个链表。后续操作都是针对这个头指针来进行的,也就是针对该链表来进行的。

创建一个结点的功能,一般不会由外部直接调用,而主要是提供给插入结点函数使用的,剥离出来更灵活,因为插入不管是头插还是尾插,都需要先创建一个结点。

注意:堆上的数据得主动释放才会没了,不要以为在函数内创建的,离开函数就会被释放,别跟栈搞混了。

创建结点有什么需要注意的地方吗?

我们使用结构体指针去进行操作,因此创建节点后返回的是链表结点的指针;

传递进入的是创建的结点中要存储的数据;

新创建的结点中下一个结点指针应该先指向NULL,后面插入时如果是尾插,那就不用管,如果是头插,那就得把指针指向上一个首结点;

创建结点的函数可以限定为static,因为一般就是提供给插入函数使用的。

插入结点时应当注意什么?

浅析线性表(链表)的头插法和尾插法的区别及优缺点_链表头插法和尾插法哪个好-CSDN博客

尾插时:

不需要返回值;

传递进入链表,也就是一个结点指针,还有一个就是插入的结点里存放的数据,这个数据一会儿在创建结点时需要使用;

插入前先要创建一个结点;

头指针是用来存储第一个结点的指针的变量;

遍历链表

有了插入和遍历,就能初步验证链表了。

链表里的数据类型其实也是固定的。

以下提供不带头结点的链表示例:

#include "linkedlist.h"
#include <stdlib.h>
#include <stdio.h>

//定义单链表结点里存放的数据类型
typedef int NODE_TYPE;

//定义链表结点
typedef struct LinkedListNode
{
    NODE_TYPE nodeData;
    struct LinkedListNode *nextNode;
}ST_LLNODE;

//创建结点
static ST_LLNODE *CreateOneNode(NODE_TYPE data)
{
    ST_LLNODE *newNode = malloc(sizeof(ST_LLNODE));//结点创建必须malloc,在堆上创建,如果用局部变量,那么出函数就没了
    
    if(newNode == NULL)
    {
        printf("warning: create newnode fail!");
        return NULL;
    }
    
    newNode->nodeData = data;
    newNode->nextNode = NULL;
    
    return newNode;
}

//尾插
void InsertNodeAtTail(ST_LLNODE **linkedList, NODE_TYPE data)
{
    if(linkedList == NULL)
    {
        return;
    }
    
    //先创建一个结点
    ST_LLNODE *newNode = CreateOneNode(data);
    
    //判断链表是不是空链表
    if(*linkedList == NULL)
    {
        *linkedList = newNode;//如果是空链表,就直接把第一个结点挂到链表下面
    }
    else
    {
        ST_LLNODE *tail = *linkedList;
        while(tail->nextNode != NULL)
        {
            tail = tail->nextNode;
        }//循环结束后,就到了最后一个结点
        
        tail->nextNode = newNode;//最后一个结点指向新结点
    }
}

//遍历链表
void PrintLinkedList(ST_LLNODE *linkedList)
{
    if(linkedList == NULL)
    {
        printf("warning: linkedlist is null!");
    }
    else
    {
        ST_LLNODE *tail = linkedList;
        //直接判断每个结点是不是空,最后一个结点的下一个肯定是空,意味着遍历结束了
        while(tail != NULL)//插入时遍历是为了找到最后一个结点,这里是为了定位到最后一个结点的下一个NULL
        {
            printf("data %d\r\n", tail->nodeData);
            tail = tail->nextNode;
        }
        printf("print is ok_____________\r\n");
    }
}

//测试链表
void Test(void)
{
    //创建一个链表
    ST_LLNODE *myLLHead = NULL;//链表头指针,这个指针以后指向第一个结点
    
    InsertNodeAtTail(&myLLHead, 0);//需要传入链表的指针,才能改变该链表,需要将该链表的NULL改成第一个结点的地址
    InsertNodeAtTail(&myLLHead, 1);//后续尾差都不用动改地址,不过头插需要更改
    InsertNodeAtTail(&myLLHead, 2);
    InsertNodeAtTail(&myLLHead, 3);
    InsertNodeAtTail(&myLLHead, 4);
    InsertNodeAtTail(&myLLHead, 5);
    InsertNodeAtTail(&myLLHead, 6);
    InsertNodeAtTail(&myLLHead, 7);
    InsertNodeAtTail(&myLLHead, 8);
    InsertNodeAtTail(&myLLHead, 9);
    InsertNodeAtTail(&myLLHead, 10);
    
    PrintLinkedList(myLLHead);//内部不需要修改指针本身,只是使用地址值,所以无需传递链表的指针
}

//再次提醒,C语言的函数如果返回的是个局部变量的地址值,我们是无法通过该地址去访问局部变量的;
//如果我们想要在一个函数内将一个局部变量传递给另一个函数,并且在另一个函数内部能影响到传递的数据,就必须传递地址值,否则传递的只是个副本(同理,返回值其实也是个副本);
//如果这个要传递的局部变量本身就是个指针,那就需要传递二重指针。(这种情况特别容易搞混)
//除非是个全局变量,此时,也不需要传参了,直接使用即可。

//头插

//删除结点

//修改结点


//再次提醒,C语言的函数如果返回的是个局部变量的地址值,我们是无法通过该地址去访问局部变量的;
//如果我们想要在一个函数内将一个局部变量传递给另一个函数,并且在另一个函数内部能影响到传递的数据,就必须传递地址值,否则传递的只是个副本(同理,返回值其实也是个副本);
//如果这个要传递的局部变量本身就是个指针,那就需要传递二重指针。(这种情况特别容易搞混)
//除非是个全局变量,此时,也不需要传参了,直接使用即可。

通常链表各结点都是在创建结点时malloc出来的,然后各结点的指针都保存在了上一个结点里面,所以,可以在删除结点时free释放结点内存。 

链表插入时是插入结点元素,结点的指针是插入时通过malloc得到的。 

网上关于链表的实现逻辑,有各种各样的, 还是先多看看总结吧。

头结点和头指针 

链表中,头指针和头结点的理解,是一个难点。

看下链表的图示:

图中的phead指针中存放的是第一个结点的地址,那么根据指着地址我们就可以找到这个结构体,又因为这个结构体中存放了下一个结构体的地址,所以又可以找到第二个结构体,循环往复就可以找到所有的结点,直到存放空地址的结构体。

注意,每一个结点的地址都在上一个结点中,那么,问题就来了,第一个结点没有上一个结点,它的指针放在哪呢?找不到第一个结点,就表示这个链表丢失了。

我们实际中最常用还是这两种链表结构:

1. 不带头结点单向链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头结点双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

链表里说的带或者不带头结点,这个头结点应该指的是固定头结点,因为就算没有一个固定头结点,链表的第一个有效结点也可以称为头结点。所以,别理解错了。我们在链表中提到头结点,约定俗成都是指的固定头结点。固定头结点中不存储数据。

除了头结点,另外还有个头指针的概念,上面提到个问题,那就是第一个结点没有上一个结点,它的指针放在哪呢?为此,我们需要定义一个指针,让这个指针指向第一个结点,注意,头指针不是结点,只是一个指针变量,只不过这个指针变量指向的是一个结点类型的数据。

参考:数据结构:单链表——带头结点与不带头结点步骤详解_单链表带头结点和不带头结点各种操作-CSDN博客

头指针:通常使用“头指针”来标识一个链表,如单链表L,头指针为NULL的时表示一个空链表。链表非空时,头指针指向的是第一个结点的存储位置。

头结点:在单链表的第一个结点之前附加一个结点,称为头结点。头结点的Data域可以不设任何信息,也可以记录表长等相关信息。若链表是带有头结点的,则头指针指向头结点的存储位置。

无论是否有头结点,头指针始终指向链表的第一个结点。如果有头结点,头指针就指向头结点(只不过头结点的数据域为空而已)。

这里就有个问题:为什么链表要区分带头结点和不带头结点两种?

答案很简单,为了方便。

具体操作上方便在哪里,我们接下来看下。

如果带头结点,那么头插操作时,也相当于是在结点之间插入结点,和其他在任意结点之间插入结点的操作是一样的;如果不带头结点,那么头插操作时,第一个结点总是在变化的,涉及的是跟头指针之间的关联,跟其他在任意结点之间插入结点的操作是不一样的。

我们知道,头指针就是第一个结点的指针pHead。如果没有固定头结点,那么在头插时,操作是这样的。

pNode->next = pHead;//先把当前第一个结点的指针,也就是pHead,挂到待插入结点的后面;
pHead = pNode;//然后重新给头指针赋值,也就是把待插入结点的指针赋值给头指针,结点指针通过malloc得到

不带头结点的链表,在表头以外的地方插入结点

pNode->next = pAimNode->next;//先把待插入位置的下一个结点指针挂到待插入结点里
pAimNode->next = pNode;//再把待插入结点挂到前一个结点后面

可见,头插和非头插时的操作是不一样的。

如果带了固定头结点,因为插入时都属于结点间的插入操作,所以操作方式和上述第二种情况都是一样的。

也就是说,带固定头结点时,统一了头插和非头插时的插入操作。

同理,第一个位置的删除和其他位置的删除操作也能统一起来。

另外,带头结点还有个好处

参考:深刻理解:带头结点和不带头结点的区别 使用头结点的优势-CSDN博客

优势2:统一空表和非空表的处理

若使用头结点,无论表是否为空,头指针都指向头结点,也就是*LNode类型,对于空表和非空表的操作是一致的。

若不使用头结点,当表非空时,头指针指向第1个结点的地址,即*LNode类型,但是对于空表,头指针指向的是NULL,此时空表和非空表的操作是不一致的。

这一点主要是统一了头指针的指向问题。

更多参考:单链表实现+注释+原码

参考:

【数据结构】链表(单链表实现+详解+原码)-CSDN博客

关注下结点命名和链表命名的问题,因为链表本身也是用一个结点指针来表示的,链表的类型声明中是否含有node关键词?

接下来参考上述链接,来关注下各功能里的一些注意点,我们以不带头结点的单链表为例。

定义如下:

typedef int SLTDataType;
 
typedef struct SListNode
{
        SLTDataType data;
        struct SListNode* next;//存放下一个结点的地址
}SListNode;

完整实现

//打印链表
//传递进来的就是头指针,头指针标记了一个链表
void SListPrint(SListNode* phead)
{
        SListNode* cur = phead;//这里不直接操作头指针,是为了不破坏头指针
        while (cur)//当数据元素存在时则打印
        {
                printf("%d->", cur->data);//头指针访问的data就是第一个结点的数据
                cur = cur->next;//依次往后遍历结点
        }
        printf("NULL\n");
}
 
 
//申请结点
//申请结点的功能一般都是独立出来给插入结点的函数调用的
//注意,只用传递进入结点的数据
//返回一个结点指针
SListNode* SListBuyNode(SLTDataType x)
{
        SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
        if (newnode == NULL)
        {
                perror("malloc:");
                exit(-1);
        }
        newnode->data = x;//将元素放进去
        newnode->next = NULL;//指向的结点指针先置NULL,然后在被调用处根据情况设置
        return newnode;
}
 
//销毁链表
//为什么这里要传递进入二重指针?
//仔细想一想,如果传递的是指针本身,那么,我们可以去操作指针所指向的内容,但是不能改变指针变量本身,
//因为这时传递的是指针的一个副本,改变了副本,也不影响原来的指针变量的值。
//如果传入二重指针,也就是指针的指针,那么,就肯定是因为想要改变传入的指针变量本身。
//销毁链表,需要改变指针变量本身吗?
void SListDestory(SListNode** pphead)
{
        assert(pphead);
        SListNode* cur = *pphead;//对二重指针取指针,得到的就是头指针,这里不是传递二重指针的原因
        while (cur)
        {
                SListNode* next = cur->next;
                free(cur);
                cur = next;
        }
        *pphead = NULL;//将头指针置NULL,这里是传递二重指针的原因,若传递头指针,就没法改变原指针
}//注意,当定义的链表头指针是个局部变量的时候,就需要传递进入二重指针;
//但是如果链表头指针是个全局变量,则无需传递二重指针;
//不管怎样,目的都是为了改变头指针本身的值。
//另外,需要传递二重指针的另一个原因是,所实现的链表是不带固定头结点的,
//如果带了固定头结点,那么就不用去改变头指针的值了,也就不需要传递二重指针了。
//所以,总结就是,二重指针仅仅是在实现不带头结点链表,并且头指针是以局部变量的形式定义的情况下才需要的,
//如果链表带了头结点,或者是以全局变量定义的,都无需传递二重指针。

//尾插
//传递进入头指针的指针,以及结点数据
//为什么尾插需要传递二重指针?尾插哪里会改变头指针的指向?当链表是空表时用到!
void SListPushBack(SListNode** pphead, SLTDataType x)
{
        assert(pphead);
        SListNode* newnode = SListBuyNode(x);
        if (*pphead == NULL)
        {
                *pphead = newnode;//空表处理,传递二重指针的理由
        }
        else
        {
                SListNode* cur = *pphead;
                while (cur->next)
                {
                        cur = cur->next;
                }
                cur->next = newnode;
        }
}

//头插
//头插时一定会改变头指针的指向,所以传递进入二重指针
void SListPushFront(SListNode** pphead, SLTDataType x)
{
        assert(pphead);
        SListNode* newnode = SListBuyNode(x);
        newnode->next = *pphead;//将当前头结点挂到新结点后面
        *pphead = newnode;//新结点成为新的头结点
}
 
//尾删
void SListPopBack(SListNode** pphead)
{
        assert(*pphead && pphead);//断言不是空表
        if ((*pphead)->next == NULL)//如果只有一个结点
        {
                free(*pphead);
                *pphead = NULL;//置为空表
                return;
        }
        SListNode* cur = *pphead;
        SListNode* next = (*pphead)->next;
        while (next->next)
        {
                next = next->next;
                cur = cur->next;
        }
        cur->next = NULL;
        free(next);
        next = NULL;
}
 
//头删
void SListPopFront(SListNode** pphead)
{
        assert(*pphead && pphead);
        SListNode* next = (*pphead)->next;
        free(*pphead);
        *pphead = next;
}
 
 
//查找
SListNode* SListFind(SListNode* phead, SLTDataType x)
{
        while (phead)
        {
                if (phead->data == x)
                {
                        return phead;
                }
                phead = phead->next;
        }
        return NULL;
}
 
//指定位置后面插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
        assert(pphead && pos);
        SListNode* newnode = SListBuyNode(x);
        SListNode* next = pos->next;
        pos->next = newnode;
        newnode->next = next;
 
}
 
//指定位置删除
void SListErase(SListNode** pphead, SListNode* pos)
{
        assert(pphead && pos);
        if (*pphead == pos)
        {
                *pphead = (*pphead)->next;
                free(pos);
                pos = NULL;
        }
        else
        {
                SListNode* cur = *pphead;
                while (cur->next != pos)
                {
                        cur = cur->next;
                }
                cur->next = pos->next;
                free(pos);
                pos = NULL;
        }
}

更多待补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值