C语言数据结构----链表

本文详细介绍了单链表的基本概念、实现原理及关键操作,包括插入、删除等,并提供了完整的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

昨天写了一篇优快云了,由于培训的地方网速太烂,所以导致写好了一篇优快云博客什么都没有了。只能说运气不好了吧,把昨天那篇静态表的博客先放下,今天先写链表的。

老唐讲的数据结构其实还是挺难的。

一、基本概念

1.链表&单链表n个结点链接成一个线性的结构叫做表链表,当每个结点只包含一个指针域时,叫做单链表。

2.链表的几个关键概念

(1)表头结点,链表中的第一结点,包含指向第一个数据元素的指针以及链表自身的一些信息。

(2)数据结点,链表中代表数据元素的结点,包含指向下一个数据元素的指针和数据元素的信息。

(3)尾结点,链表中的最后一个数据结点,其的下一个元素指针为空,也就是这个结点再没有后继了,所以称为尾结点。

在基本的链表插入操作中有头插入方式和尾插入方式。操作方式图所示:

头插入方式:

尾插法建表:

在链表中,一定要注意头的信息和头所指向的数据结点。同时,老唐说要建立没有头的链表,我不清楚。

单链表的存储结构上面已经说过了,下面是图示:

 

 

二、头结点,结点指针域和数据元素的具体实现(老唐的大招)

链表中一个数据结点的包含这个数据结点自身的数据部分以及指向下一个数据结点的指针部分。如下图所示:

显然,链表中的一个数据元素肯定是一个结构体构成的,只有这样,才可以保存一个数据结点中的两个部分。其中,sum可以是任意类型的数据,p是指向下一个数据结点的指针,其中p的类型是unsigned int,因为是地址所以刚好占用四个字节。

这个时候老唐提出来了一个问题,为什么这个数据元素的地址不妨在具体的指针类型中,因为放在具体的指针类型中我们也是通过操作后来的next来操作数据结点中的数据,把地址规定成为unsigned int 类型,也是存放的四个字节的地址,所以是一样,同时略显简洁。

老唐的第二个问题是,顺序表的实现为什么要保存具体的指针地址而不是数据元素,我想问,假如你插入的数据结点里面有多个数据元素那该怎么办?那会占用多大的内存空间呢?所以保存地址最直接的原因就是为了节省内存空间。

同时,假如我们想要在链表中插入不同数据类型的元素,那么我们应该怎么办呢?你可能会说,那就插入吧,但是其实并不是那么简单的。

假如你插入的第一个数据结点的表示的结构体是下面这个:

struct Value
{
    LinkListNode header;
    int v;
};

那么第二个数据结点的表示结构体是如下这个:

struct Value
{
    LinkListNode header;
    char c;
};

你每次插入都会发生数据结点指针的变化,这样的话我们时刻都要想着这个结点指针。这个时候老唐来大招了。

针对LinkListNode* current = (LinkListNode*)sList;
 在lmain.c中,struct Value
  {
     LinkListNode header;
     int v;
  };
我们要放入链表里的数据类型是上面这个结构体,但是假如我们要改变放入接结构体中的数据类型,那么我们就要把next 这个类型作为中间变量,这样无论我们要放进来的是什么变量,都被转换成为这样的类型,那么就可以随意插入了。
  struct Value
  {
     LinkListNode header;
     int v;
  };
  这个结构体位于main.c中,这个是要插入链表中的结构体,这个结构体在进行插入的时候执行LinkList_Insert(list, (LinkListNode*)&v1, LinkList_Length(list));这个语句。这样(LinkListNode*)&v1就把这个结构体的地址转为了LinkListNode*类型。
  typedef struct _tag_LinkListNode LinkListNode;
  struct _tag_LinkListNode
  {
      LinkListNode* next;
  };
这个时候,我们要插入的节点已经转为了上面这个结构体类型。又因为LinkListNode* next这样当我们操作要插入数据结点的时候,我们可以直接操作next来实现。同时,我们也可以任意的换结构体里的成员类型,因为我们都要把它的地址转为LinkListNode*这个类型,进而通过next 指针来操作原来的结构体地址。这应该就是老唐所说的数据封装。
这里面涉及一个东西,那就是当一个大结构体被强制类型转换为一个小类型的结构体的时候,是
直接卡下来的,但是这个时候需要满足的条件是:嵌套的结构体必须要放在大结构体的前方。
这个时候操作卡下来的结构体的前四个字节的地址就是在操作原来的结构体,也就是我们要插入的数据结点。

上面的东西我觉得真心很绕,不过,你就记住一个原则,老唐之所以这么做的原因就是想通过这种方法,把next变成一个中间变量,这样,它不论上层要操作链表的程序传进来的是什么值,我们都可以通过操作next这个中间变量来操作链表中的数据结点,也就是说可以在链表中进行数据结点的操作了。

三、代码部分

1.链表操作的.c文件

#include <stdio.h>
#include <malloc.h>
#include "LinkList.h"



//由于是复用表,所以应该改变的是地址,而一个地址为4位,所以一个unsigned int
//刚刚好,也就是向顺序表里插入地址。
typedef unsigned int TSeqListNode;
typedef struct _tag_LinkList
{
    LinkListNode header;
    int length;
} TLinkList;

LinkList* LinkList_Create() // O(1)
{
		//这里是给TLinkList这个结构体开辟空间
    TLinkList* ret = (TLinkList*)malloc(sizeof(TLinkList));
    
    //判定是否给TLinkList结构体分配了空间
    if( ret != NULL )
    {
        ret->length = 0;
        //ret是TLinkList这个结构体成员的指针
        //这个指针指向的header是这个结构体的一个内容
        //这个内容是一个结构体,这个内容结构体的的一个内容next赋值为NULL。
        //这里是将指向下一个节点的指针赋值为空。
        ret->header.next = NULL;
    }
    
    return ret;
}

void LinkList_Destroy(LinkList* list) // O(1)
{
		//在malloc.h中声明
    free(list);
}

void LinkList_Clear(LinkList* list) // O(1)
{
		//进行强制类型转换
    TLinkList* sList = (TLinkList*)list;
    
    //判定传入的值是否有效
    if( sList != NULL )
    {
        sList->length = 0;
        sList->header.next = NULL;
    }
}

/*获取链表的长度*/
int LinkList_Length(LinkList* list) // O(1)
{
    TLinkList* sList = (TLinkList*)list;
    /*开始定义为-1,如果传进来的指针为空,则返回-1*/
    int ret = -1;
    
    if( sList != NULL )
    {
        ret = sList->length;
    }
    
    return ret;
}

int LinkList_Insert(LinkList* list, LinkListNode* node, int pos) // O(n)
{ 
    TLinkList* sList = (TLinkList*)list;
    
    int ret = (sList != NULL) && (pos >= 0) && (node != NULL);
    int i = 0;
    
    if( ret )
    {
    		//这一步其实是在把头指针转化为LinkListNode*类型,并把链表头的地址赋值为current.
    		//这里也可以说是定义current指针,让我们的current指向我们的头结点。
        LinkListNode* current = (LinkListNode*)sList;
    		//判断是否到达单链表尾结点的地方    
        for(i=0; (i<pos) && (current->next != NULL); i++)
        {
            current = current->next;
        }
        
        /*
        		这里是把要插入的节点位置的下一个结点的地址赋值给了要插入的
        		同时,把node这个大类型的指针指向了原来current->next应该指向的位置。
        */
        
        //先把指向的指针进行移交
        node->next = current->next;
        //再把新内容的值赋给原来应该指向这个位置的指针
        current->next = node;
        //通过上面这两步,完成了一个链表不断,同时还完成了插入一个新的结点的步骤。
        
        sList->length++;
    }
    
    return ret;
}

LinkListNode* LinkList_Get(LinkList* list, int pos) // O(n)
{
		//进行一步强制类型转化
    TLinkList* sList = (TLinkList*)list;
    LinkListNode* ret = NULL;
    int i = 0;
    
    if( (sList != NULL) && (0 <= pos) && (pos < sList->length) )
    {
        LinkListNode* current = (LinkListNode*)sList;
        
        //对目标位置开始遍历,当i = pos的时候遍历结束。
        for(i=0; i<pos; i++)
        {
            current = current->next;
        }
        
        ret = current->next;
    }
    
    return ret;
}

LinkListNode* LinkList_Delete(LinkList* list, int pos) // O(n)
{
    TLinkList* sList = (TLinkList*)list;
    LinkListNode* ret = NULL;
    int i = 0;
    
    if( (sList != NULL) && (0 <= pos) && (pos < sList->length) )
    {
        LinkListNode* current = (LinkListNode*)sList;
        
        for(i=0; i<pos; i++)
        {
            current = current->next;
        }
        
        ret = current->next;
        current->next = ret->next;
        
        sList->length--;
    }
    
    /*
    	这个返回值的含义是假如在主函数中malloc了一个数据结点的空间,
      那么在删除这个结点的时候必须把这个结点的指针返回给主函数来进行free.
    */
    return ret;
}

2.链表的.h文件

#ifndef _LINKLIST_H_
#define _LINKLIST_H_

typedef void LinkList;
typedef struct _tag_LinkListNode LinkListNode;
struct _tag_LinkListNode
{
    LinkListNode* next;
};

LinkList* LinkList_Create();

void LinkList_Destroy(LinkList* list);

void LinkList_Clear(LinkList* list);

int LinkList_Length(LinkList* list);

int LinkList_Insert(LinkList* list, LinkListNode* node, int pos);

LinkListNode* LinkList_Get(LinkList* list, int pos);

LinkListNode* LinkList_Delete(LinkList* list, int pos);

#endif

3.测试代码:

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

/* run this program using the console pauser or add your own getch, system("pause") or input loop */

struct Value
{
    LinkListNode header;
    int v;
};

int main(int argc, char *argv[]) 
{
    int i = 0;
    LinkList* list = LinkList_Create();
    
    /*要插入链表中的成员*/
    struct Value v1;
    struct Value v2;
    struct Value v3;
    struct Value v4;
    struct Value v5;
    
    v1.v = 1;
    v2.v = 2;
    v3.v = 3;
    v4.v = 4;
    v5.v = 5;
    
    /*头插法建立链表,这个时候的插入的元素慧顺序的向后移动*/
    LinkList_Insert(list, (LinkListNode*)&v1, LinkList_Length(list));
    LinkList_Insert(list, (LinkListNode*)&v2, LinkList_Length(list));
    LinkList_Insert(list, (LinkListNode*)&v3, LinkList_Length(list));
    LinkList_Insert(list, (LinkListNode*)&v4, LinkList_Length(list));
    LinkList_Insert(list, (LinkListNode*)&v5, LinkList_Length(list));
    
    for(i=0; i<LinkList_Length(list); i++)
    {
        struct Value* pv = (struct Value*)LinkList_Get(list, i);
        
        printf("%d\n", pv->v);
    }
    
    while( LinkList_Length(list) > 0 )
    {
        struct Value* pv = (struct Value*)LinkList_Delete(list, 0);
        
        printf("%d\n", pv->v);
    }
    
    LinkList_Destroy(list);
    
    return 0;
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值