【数据结构】链表

目录

1 链表的概念及结构

2 链表的分类

3 单向链表的实现(C语言)

一、定义链表的结点

二、链表的打印

三、链表的尾插

四、链表的头插

五、链表的尾删

六、链表的头删

七、链表的查找

八、在 pos 位置的前面插入

九、在 pos 位置的后面插入

十、在 pos 位置删除

十一、在 pos 后面删除

十二、链表的销毁

4 双向循环链表的实现(C语言)

一、定义双向循环链表的结点

二、双向循环链表的初始化

三、 双向循环链表的打印

四、 双向循环链表的尾插

五、判断双向循环链表是否为空

六、双向循环链表的尾删

七、双向循环链表的头插

八、在 pos 位置之前插入


1 链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

物理结构:(数据实际上在内存中的内存位置,指针的值的变化)

逻辑结构:(为了便于理解,将指针形象化为箭头,指针值的变化反映为指针的移动)


2 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向
 

2.戴头或者不戴头

3.循环或者非循环

 虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。


 3 单向链表的实现

要实现的接口:

// 动态申请一个节点
SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDataType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 在pos的前面插入
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDataType x);
// 删除pos位置
void SLTErase(SListNode** pphead, SListNode* pos);
// 删除链表
void SLTDestroy(SListNode** pphead);

 一、定义链表的结点

typedef int SLTDataType;

typedef struct SListNode
{
    SLTDataType data;
    struct SListNode* next;
}SLTNode;

typedef int SLTDataType; 作用:如果想要链表存储的数据类型是 float ,只需要修改这条语句的 int 就行了。另外,有人提出让链表的头结点的 data 存储链表的结点个数,根据链表结点的定义,这样做是错误的,因为 data 是 SLTDataType 类型,若该类型是 char ,当链表结点的个数超过128 个,就会出现问题。

不能写成:

typedef int SLTDataType;

typedef struct SListNode
{
    SLTDataType data;
    SLTNode* next;
}SLTNode;

typedef 在定义完结构体后才起作用。 

在继续展示下面的函数前,有必要弄清什么时候该使用 assert 断言指针为 NULL,什么时候不该使用。

该用的时候:实参指针为空是错误的时候。也就是说,实参指针永远不应该为空,为空是错误情况,这个时候就应该使用 assert 断言。

不该使用的情况:实参指针可以为空的时候。也就是说,实参指针为空代表一定的意义的时候,这个时候不应该使用 assert 断言。

二、链表的打印

void SLTPrint (SLTNode* phead)
{
    SLTNode* cur = phead; 
    while (cur != NULL)
    {
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

易错点

1、phead 是指向链表第一个结点的指针,可以为 NULL(表示链表没有数据),所以该函数不需要断言 phead 指针为空。但是对于顺序表,顺序表没有数据的标志是 size = 0,而不是 ps = NULL,所以打印顺序表的数据需要断言 ps

2、cur = cur->next;不能写成 cur++;除非 cur 是指向一个结构体数组,链表的结点是非连续的。 

3、while(cur != NULL) 不能写成 while(cur->next != NULL) ,会导致最后一个数据没有打印。(while(cur != NULL) 也可以写成 while(cur))

三、链表的尾插

void SLPushBack(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); 
    if (newnode == NULL)
    {
        perror("malloc fail");
        return;
    }
    newnode->data = x; 
    newnode->next = NULL;

    if(*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        // 找尾
        SLTNode* tail = *pphead; 
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        tail->next = newnode;
    }
}

易错点

1、不能断言 phead,phead == NULL 表示链表无数据,是合法的。

2、找尾部分不能写成:(这样链表链接不上)

// 找尾
SLTNode* tail = *pphead; 
while (tail != NULL)
{
    tail = tail->next;
}
tail = newnode;

3、if(*pphead == NULL)的判断十分有必要,不能省略。

4、要改变 phead 的内容,函数的参数应该是二级指针。如果不想使用二级指针,可以将函数的返回值的类型设计为 SLTNode* ,返回新的头指针。

5、由于 pphead 是指向 phead 的,pphead 应该永远不为 NULL,但为了避免疏忽大意带来的调试麻烦,有 pphead 为实参的函数都应该断言 pphead。

接下来要多次用到创建结点的操作,将以上创建结点的代码封装成一个函数:

SLTNode* BuyNode(SLTNode** pphead,int x)
{
    assert(pphead);
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); 
    if (newnode == NULL)
    {
        perror("malloc fail");
        return;
    }
    newnode->data = x; 
    newnode->next = NULL;

    return newnode;
}

四、链表的头插

void SLTPushFront(SLTNode ** pphead, SLTDataType x)
{
    assert(pphead);
    SLTNode* newnode = BuySLTNode(x); 
    newnode->next = *pphead; 
    *pphead = newnode;
}

以上代码也能应对链表为空的情况

逻辑结构:

 对比尾插会发现链表头插的效率更高。 

 五、链表的尾删

void SLTPopBack (SLTNode** pphead)
{
    assert(pphead);
    //链表为空
    //温柔的检查
    //if(*PPhead == NULL)
    //{
    //    return;
    //}
    //暴力的检查
    assert(*pphead != NULL);
    if(*pphead->next == NULL)
    {
        free(*PPhead);
        *pphead = NULL;
        return;
    }

    // 找尾
    SLTNode* prev = NULL; 
    SLTNode* tail = *pphead; 
    while (tail->next != NULL)
    {
        prev = tail; 
        tail = tail->next;
    }
    free(tail); 
    tail = NULL;
    prev->next = NULL;
}

注意点:

1、在找尾的过程中,tail 在指向下一个结点之前,把 tail 赋值给 prev ,便于在找到最后的结点时将最后的结点的上一个结点的 next 指针置为空。

2、结点的存储空间在堆区,删除结点后要释放空间。

3、要考虑到只有一个结点和链表为空的情况,用 if 语句判断一下。

另一种更巧妙的办法:

void SLTPopBack (SLTNode** pphead)
{
    assert(pphead);
    //链表为空
    //温柔的检查
    //if(*PPhead == NULL)
    //{
    //    return;
    //}
    //暴力的检查
    assert(*pphead != NULL);
    if(*pphead->next == NULL)
    {
        free(*PPhead);
        *pphead = NULL;
        return;
    }

    // 找尾
    SLTNode* tail = *pphead; 
    while (tail->next->next != NULL)
    {
        tail = tail->next;
    }
    free(tail->next); 
    prev->next = NULL;
}

六、链表的头删

void SLTPopFront(SLTNode ** pphead)
{
    assert(pphead);
    // 暴力检查
    assert(*pphead);
    // 温柔的检查
    //if (*pphead == NULL) 
    //return;
    SLTNode* first = *pphead; 
    *pphead = first->next; 
    free(first); 
    first = NULL;
}

对比尾删会发现链表头删的效率更高。 

七、链表的查找

SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
    SLTNode* cur = phead; 
    while (cur)
    {
        if (cur->data == x)
        {
            return cur;
        }
    cur = cur->next;
    }
    return NULL;
}

注意:空链表也能查找,只是返回空指针。 

八、在 pos 位置的前面插入

void SLTInsert(SLTNode ** pphead, SLTNode* pos, SLTDataType x)
{
    assert(pphead);
    assert(pos);
    if (pos == * pphead)//在第一个位置插入,不就是头插吗
    {
        SLTPushFront(pphead, x);
    }
    else
    {
        // 找到pos的前一个位置
        SLTNode* prev = * pphead; 
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        SLTNode* newnode = BuySLTNode(x); 
        prev->next = newnode; 
        newnode->next = pos;
    }
}

逻辑结构:

 注意:

1、pos 位置是否在链表上是函数使用者考虑的事,上面的函数不保证 pos 位置是否在链表上,但应该保证 pos 至少不是 NULL。

2、在 pos 位置的后面插入的复杂性比在前面插入的复杂性小,因此在 pos 位置的前面插入的问题可以转化为在 pos 后面插入然后交换 pos 和插入的数据的位置。

九、在 pos 位置的后面插入

avoid SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    assert(pos); 
    SLTNode* newnode = BuySLTNode(x); 
    newnode->next = pos->next; 
    pos->next = newnode;
}

pos 指针指向链表的一个结点,该结点的地址由链表的查找函数(上面的第八个函数)返回。

以上代码不能写成:

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    assert(pos); 
    SLTNode* newnode = BuySLTNode(x); 
    pos->next = newnode; 
    newnode->next = pos->next;
}

这会导致 nownode 的 next 成员指向的是它自己。逻辑结构:

十、在 pos 位置删除

void SLTErase(SLTNode ** pphead, SLTNode* pos)
{
    assert(pphead); 
    assert(pos); 
    //assert(*pphead);
    if (*pphead == pos)
    {
        SLTPopFront(pphead);//就是前删
    }
    else
    {
        // 找到pos的前一个位置
        SLTNode* prev = * pphead; 
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        prev->next = pos->next; 
        free(pos);
    }
}

注意:在调用完该函数后,一般由该函数的调用者来 free(pos)。

十一、在 pos 后面删除

void SLTEraseAfter (SLTNode* pos)
{
    assert(pos); 
    assert(pos->next);
    SLTNode* del = pos->next;//保存pos位置下一个结点的地址
    pos->next = pos->next->next; 
    free(del); 
    del = NULL;
}

以上代码不能写成:

void SLTEraseAfter(SLTNode* pos)
{
    assert(pos); 
    assert(pos->next);
    pos->next = pos->next->next;
}

这会导致 pos 后面的结点的地址找不到。

十二、链表的销毁

void SLTDestroy (SLTNode* phead)
{
    SLTNode* cur = phead; 
    while (cur)
    {
        SLTNode* tmp = cur->next; 
        free(cur); 
        cur = tmp;
    }
}

4 双向循环链表的实现

双向循环链表的初始形态应该是怎样呢?也就是说,如果一个双向循环链表没有数据,它该是怎样呢?对于顺序表来说,没有数据的标志是 size == 0,对于单向非循环链表来说,没有数据的标志是 phead == NULL,那对于双向循环链表来说,应该是怎样呢?

会是以下的样子吗?

如果真是这样,就没有体现循环的特点了,所以应该是这样:

一、定义双向循环链表的结点

typedef int LTDataType;

typedef struct ListNode
{
    struct ListNode* next; 
    struct ListNode* prev;
    LTDataType data;
}LTNode;

二、双向循环链表的初始化

void LTInit(LTNode** pphead)
{
    *pphead = BuyListNode(-1); 
    *phead->next = phead; 
    *phead->prev = phead;
}

如果不想使用二级指针,可以将函数的返回值类型设计为  LTNode* 类型,返回头指针 phead。

LTNode* LTInit ()
{
    LTNode* phead = BuyListNode(-1); 
    phead->next = phead; 
    phead->prev = phead;
    
    return phead;
}

三、 双向循环链表的打印

void LTPrint(LTNode* phead)
{
    assert(phead);
    LTNode* cur = phead->next; 
    printf("<=head=>");
    while (cur != phead)
    {
        printf("%d<=>", cur->data); 
        cur = cur->next;
    }

    printf("\n");
}

注意:cur 指针开始时指向 head 的下一个结点,当 cur 指向 head 时停止打印。 

四、 双向循环链表的尾插

void LTPushBack (LTNode* phead, LTDataType x)
{
    
    assert(phead);

    LTNode* newnode = BuyListNode(x); 
    LTNode* tail = phead->prev;

    tail->next = newnode; 
    newnode->prev = tail; 
    newnode->next = phead; 
    phead->prev = newnode;

}

1、相比于单向链表的尾插,双向循环链表的尾插不需要判断链表是否为空,不需要找尾,不需要使用二级指针(函数中改变的都是结构体的成员)十分方便。

2、要断言 phead,因为 phead 永远不能为空,phead 为空是错误情况。

上面使用的 BuyListNode 函数:

LTNode* BuyListNode(LTDataType x)
{
    LTNode* node = ( LTNode*)malloc (sizeof (LTNode) ); 
    if (node == NULL)
    {
        perror("malloc fail"); 
        return NULL;
    }
   
    node->next = NULL;
    node->prev = NULL; 
    node->data = x;

    return node;
}

在展示双向循环链表的尾删的函数之前,先展示判断双向循环链表是否为空的函数:

五、判断双向循环链表是否为空

bool is_LTEmpty (LTNode* phead)
{
    assert(phead);

    /*if (phead->next == phead)
    {
        return true;
    }
    else
    {
        return false;
    }*/

return phead->next == phead;

该函数可以直接返回 phead->next == phead 。

六、双向循环链表的尾删

void LTPopBack (LTNode* phead)
{
    assert(phead); 
    assert(!is_LTEmpty(phead));
    
    LTNode* tail = phead->prev; 
    LTNode* tailPrev = tail->prev;
    tailPrev->next = phead; 
    phead->prev = tailPrev; 
    
    free(tail); 
    tail = NULL;
}

注意:assert( ! is_LTEmpty(phead)); !不要忘记写,assert ()括号内表达式为假才会报错。

七、双向循环链表的头插

void LTPushFront (LTNode* phead, LTDataType x)
{
    assert(phead);

    LTNode* newnode = BuyListNode(x); 
    newnode->next = phead->next; 
    phead->next->prev = newnode;
    phead->next = newnode; 
    newnode->prev = phead;
}

注意:不能一开始就 phead->next = newnode,因为这样原先的第一个数据的地址就不好找到,正确的做法是先将 newnode->next = phead->next; 

八、在 pos 位置之前插入

void LTInsert(LTNode* pos, LTDataType x)
{
    assert(pos);
    
    LTNode* prev = pos->prev; 
    LTNode* newnode = BuyListNode(x);
   
    prev->next = newnode; 
    newnode->prev = prev;
    
    newnode->next = pos; 
    pos->prev = newnode;
}

有了上面的函数,双向循环链表的头插函数将大大简化:

void LTPushFront (LTNode* phead, LTDataType x)
{
    assert(phead);

    LTInsert(phead->next,x);
}

尾插函数也是,但尾插函数的 pos 是 phead :

void LTPushBack (LTNode* phead, LTDataType x)
{
    
    assert(phead);

    LTInsert(phead,x);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值