线性链式存储结构c语言建立,线性表的链式存储结构(C语言版)

上一篇博文我对数据结构中线性表的顺序存储结构顺序表(http://12172969.blog.51cto.com/12162969/1916336)按照我的理解做了总结,今天我继续对顺序表的另一种存储结构,链表谈一下我看法。

链表为什么会出现?按照我的理解,有需就有求,我们都知道,当顺序表存储的元素较多时,插入删除,就要耗费大量时间,这是不能满足当下的需求的,而链表就解决了这个问题,也不是说链表就是最好的没有问题了,链表是用空间换时间,牺牲空间,换取存储的方便,链表的读取元素操作就没有顺序表操作方便了,所以在一种特定的情况下,合适的才是最好的。ok,我们先来明确几个基本概念:

头指针:链表中第一个节点的存储位置叫头指针。

头结点:第一个结点前放的一个不存储任何数据的结点。

头结点与头指针的区别:

1、头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针;

2、头指针具有标识作用,所以常用头指针冠以链表的名字;

3、无论链表是否为空,头指针均不为空。头指针是链表的必要元素。

4、头结点是为了操作的统一和方便而设立的,放在第一元素的借点之前,其数据域一般无意义(也可存储链表的长度);

5、有了头结点,对在第一个元素节点点之前插入结点和删除第一个结点,其他操作与其他结点的操作统一了;

6、头结点不一定是链表的必要元素

文字描述还是很抽象,让我们来看图,这样更直观:

3878a981dd555bb428f935d0cf5a6da9.png

理清了这几个概念,接下来我们开始用c语言实现单链表的基本操作(这里会把带头结点与不带头结点分别实现并区别两者的异同):

准备工作:定义节点类型,一些声明:

#pragma once

#define ElemType int

#define TRUE    1

#define FALSE   0

#define OK      1

#define ERROR   0

typedef int Status;

typedef int DataType;

//定义结点类型

typedef  struct Node

{

ElemType data;

struct Node  *next;

}SeqNode;

typedef  struct Node *LinkList;//定义指针类型

初始化:

//带头结点的初始化,建立一个只有头结点的空链表,头结点的数据域记录表长,并且头结点不计入长度。

//初始化成功返回OK,失败返回ERROR

Status Init_Head_SeqNode(LinkList *Head)

{

*Head = (LinkList)malloc(sizeof(SeqNode));

if((*Head) == NULL)

{

printf("out  of  memory\n");

return ERROR;

}

(*Head)->next = NULL;

(*Head)->data = 0;

return OK;

}

//不带头结点的初始化,建立一个只有头结点的空链表

Status  Init_SeqNode(LinkList *Head)

{

*Head = NULL;

return TRUE;

}

头插:下边分别是带头结点与不带头结点的头插方式,直接上代码,可能有些抽象,我自己画了一些图,来帮助理解,希望可以也可以帮助大家理解:

带头结点的头插:

cbbbd48704ee138ab9a5fdce06735a86.png

不带头结点的头插:

93d7678dd3176c6fd9e00410b19a7bab.png

头插代码:

//带头结点头插,插入成功返回TRUE,失败返回ERROR

Status Insert_Head_Fr(LinkList Head,ElemType x)

{

LinkList new_node; //保存新申请的结点

new_node = (LinkList)malloc(sizeof(Node));

if(NULL == new_node)

{

printf("out of memory\n");

return FALSE;

}

new_node->data = x;//把要插入的值赋值给新申请的结点的数据域

new_node->next = Head->next;//第一步:把头结点的下一元素即就是首元结点地地址赋给新的结点

Head->next = new_node;//第二步:把新申请的结点赋值給头结点

(Head->data)++;//带头结点的头节点的数据域用来保存链表的个数,每添加一个元素加1

return TRUE;

}

//不带头结点的头插,插入成功返回TRUE,失败返回FALSE

Status Isert_No_Head_Node(LinkList *Head,ElemType x)

{

LinkList new_node;//保存新申请的结点

new_node = (LinkList)malloc(sizeof(Node));

if(NULL == new_node)

{

printf("out of memory\n");

return FALSE;

}

new_node->data = x;

new_node->next = (*Head);

//第一步:先让新申请的结点指向首元结点,因为不带头结点的链表头指针保存首元结点的地址,那么就需要这样赋值

(*Head) = new_node;//第二步:让头指针指向新申请的结点。

return TRUE;

}

尾插:同样我也给出一些我自己绘的图:

带头结点的尾插:

a0c2eb761c3b9fb025dafbe4e35c7313.png

不带头结点的尾插:

179a5ac85f103c49c0bc7b45fc6a8b49.png

尾插代码:

/*不带头结点的与带头结点的进行尾插的时候,有人会认为两者是没有区别的,

**我想你可能忽略了当链表中没有元素时两着的插入还是有区别的。

*/

//带头结点尾插,插入成功返回TRUE,失败返回ERROR

Status Insert_Head_Ba(LinkList Head,ElemType  x)

{

LinkList new_node;                           //保存新申请的结点

LinkList temp = Head;                         //找到尾结点

new_node = (LinkList)malloc(sizeof(Node));

if(NULL == new_node )

{

printf("out of memory\n");

return FALSE;

}

//利用临时变量遍历所有链表,找到尾结点,因为temp本身是指向当前节点的next的,所以当temp等于NULL时就到了链表的最后一个结点

while(NULL != temp->next)

{

temp = temp->next;

}

new_node->data = x;

new_node->next = NULL;

temp->next = new_node;

Head->data++;//带头结点的头节点的数据域用来保存链表的个数,每添加一个元素加1

return TRUE;

}

//不带头结点尾插,插入成功返回TRUE,失败返回ERROR

Status Insert_No_Head_Ba(LinkList *Head,ElemType x)

{

LinkList new_node;

LinkList temp = (*Head);//保护头指针

new_node = (LinkList)malloc(sizeof(Node));//为新申请的结点分配空间

if(NULL == new_node)

{

printf("out of menory\n");

return FALSE;

}

if(NULL == (*Head))//空链表

{

new_node->next = NULL;

new_node->data = x;

(*Head) = new_node;

return TRUE;

}

else

{

//利用临时变量遍历所有链表,找到尾结点,因为temp本身是指向当前节点的next的,所以当temp等于NULL时就到了链表的最后一个结点了

while(NULL != temp->next)

{

temp = temp->next;

}

new_node->data = x;

new_node->next = NULL;

temp->next = new_node;

return TRUE;

}

}

按照位置插入:同样先看图:

带头结点的按位置插入:

18a21ce9154ab97d052db44e27ffe738.png

不带头结点按照位置插入:

7e5d80e0d165b57494ba59b1ccaebb70.png

//带头结点的按照位置插入如元素,头节点不计入位置,插入时分为空链表和非空链表,由于带头结点所以所有操作是一样的

//插入成功返回TRUE,并且头结点的数据元素加1,插入失败返回FALSE。

Status Insert_Head_Pos_SeqNode(LinkList Head, ElemType x, int pos)

{

LinkList temp = Head;//保护头指针

LinkList temp_pre = Head;//保存定位的pos位置地址

LinkList new_node;

new_node = (LinkList)malloc(sizeof(Node));//为新申请的结点分配空间

if(NULL == new_node)

{

printf("out of memory\n");

return FALSE;

}

if(pos > Head->data)

{

printf("插入位置出错,%d不能插入\n",x);

return FALSE;

}

//通过遍历找到要放数据位置的结点,但是由于单链表的当前结点只能找到下一个结点,

//而插入数据时,需要知道当前结点的位置,所以就需要定义一个变量来保存当前前结点的前一个结点

for(int i = 0; i < pos;i++)

{

temp = temp->next;

temp_pre = temp_pre->next;

}

new_node->data = x;//为新申请的节点的数据域赋值

new_node->next = temp;

//第一步:首先让新结点指向要插入的位置,也就是当前结点的前一个结点保存的位置

temp_pre->next = new_node;//第二步:让当前结点的前一个结点指向指向新结点

Head->data++;//插入成功,链表个数加1

return TRUE;

}

//不带头结点的按位置插入,分为两种情况,如果是空链表,就要修改头指针的指向,那么修改指针的指向,就要用二级指针接收

//如果不是空链表,只需要遍历链表,找到位置,这两种情况都要考虑插入位置是否正确

//插入成功返回TRUE,插入失败返回FALSE。

Status Inser_No_Head_Pos_SeqNode(LinkList *Head, int pos, ElemType x)

{

int Num = 0;//计算链表一共多少个结点

LinkList temp = (*Head);//保护头指针

LinkList temp_pre = (*Head);//保存pos 位置的地址

LinkList new_node;

new_node = (LinkList)malloc(sizeof(Node));//为新申请的结点分配空间

if(NULL == new_node )//防止开辟内存失败

{

printf("out of memory\n");

return FALSE;

}

//处理空链表的情况

if(NULL == (*Head) )

{

if(pos > 0)

{

printf("插入位置出错,%d不能插入\n",x);

return FALSE;

}

else

{

new_node->data = x;//为开辟的新结点赋值

new_node->next = NULL;//第一步:先让新申请的节点指向要插入 的结点的下一个结点

(*Head) = new_node;//让头指针指向新结点

return TRUE;

}

}

//处理非空链表的情况

else

{

//通过遍历找到要放数据位置的结点,但是由于单链表的当前结点只能找到下一个结点,

//而插入数据时,需要知道当前结点的位置,所以就需要定义一个变量来保存当前前结点的前一个结点

while(NULL != temp)

{

Num++;

temp = temp->next;

}

if(pos+1 > Num)//如果插入位置出错直接跳出

{

printf("插入位置出错,%d不能插入\n",x);

return FALSE;

}

else if(0 == pos)

{

new_node->data = x;//为开辟的新结点赋值

new_node->next = (*Head);

(*Head) = new_node;

}

else

{

//当遍历确定pos位置正确后,此时几个指针的指向已经发生改变,需要重新指向头指针,遍历找到需要插入的结点

temp = *Head;

for(int i = 0; i < pos - 1; i++)//定位到要删除的结点的前一个结点

{

temp = temp->next;

}

new_node->data = x;//给新结点的数据域赋值

new_node->next = temp->next;

//第一步:首先让新结点指向要插入的位置,也就是当前结点的前一个结点保存的位置

temp->next = new_node;//  第二步:让当前结点的前一个结点指向指向新结点

}

}

return TRUE;

}

头删:对与删除操作,与插入操作类比,不在画图,两者是类同的。

//不带头结点的头删,需要考虑链表是否为空链表,或者或一个元素的时候,当只有一个元素的时候,头删除就需要,修改头指针的指向,其他地方的删除,顺序都一样,用x放回删除的数据

//删除成功返回TRUE,失败返回FALSE

Status Delite_No_Head_SeqNode(LinkList *Head,ElemType *x)

{

LinkList temp_node = (*Head);//防止头指针被修改

if(NULL == temp_node)

{

printf("链表已空,已经没有元素可以删除了\n");

return FALSE;

}

if(NULL == temp_node->next)

{

*x = temp_node->data;//把要删除的结点的值保存的x中去

(*Head) = NULL;

free(temp_node);//释放删除的结点,防止内存泄露

temp_node = NULL;

return TRUE;

}

else//处理链表中有一个以上的结点的删除

{

*x = temp_node->data;//把要删除的结点的值保存的x中去

*Head = temp_node->next;//修该头指针的指向

free(temp_node);//释放删除的结点,防止内存泄露

temp_node = NULL;

return TRUE;

}

}

//带头结点的头删,不论是有一个元素还是多和元素,删除操作都不需要修该头指针指向,因而也就不需要修该头指针指向,用x放回删除的数据

//删除成功返回TRUE,失败返回FALSE;

Status Delite_Head_Fr_SeqList(LinkList Head,ElemType *x)

{

LinkList temp = Head->next;//定义临时变量防止头指针被修改

if(NULL == temp->next)

{

printf("链表已空,已经没有元素删除\n");

return FALSE;

}

*x = temp->next->data;//把要删除的结点的值保存的x中去

temp = temp->next;//第一步:让临时结点指向要删除的结点的下一个结点

free(Head->next);//第二步:释放首元结点的内存,防止内存泄露

Head->next = temp;//第三步:修改头指针的指向

return TRUE;

}

尾删:同样也不在给出示意图:对比尾插的图。

//带头结点的尾删,因为有头结点所有操作都一样

Status Delite_Head_Br_SeqNode(LinkList Head,ElemType *x)

{

//定义临时变量防止头指针被修改  ,首先让指向首元结点,尾删用它来定位最后一个结点,

//由于单链表,只知道当前节点的下一个结点的位置,那么,就需要定义一个变量保存当前结点的地址

LinkList temp = Head->next;//定位最后一个指针

LinkList temp_node_pre = Head;//保存最后一个结点的地址

if(NULL == temp)

{

printf("链表已空,已经没有元素删除\n");

return FALSE;

}

while(NULL != temp->next)

{

temp = temp->next;

temp_node_pre = temp_node_pre->next;

}

*x = temp->data;//保存最后一个结点的数据域的值

temp_node_pre->next = NULL;//修改原来结点的倒数第二个结点的指向

free(temp);//此时temp_node指向最后一个结点,防止内存泄露,释放最后一个结点的内存

temp =NULL;

return TRUE;

}

//不带头结点的尾删,由于当链表只有一个元素时,删除最后一个结点就要修改指针指向,因而就要用二级指针接收头指针,用x放回删除的数据

//删除成功返回TRUE,失败返回FALSE;

Status Delite_No_Head_Br_SeqNode(LinkList *Head,ElemType *x)

{

//定义临时变量防止头指针被修改  ,首先让指向首元结点,尾删用它来定位最后一个结点,

//由于单链表,只知道当前节点的下一个结点的位置,那么,就需要定义一个变量保存当前结点的地址

LinkList temp_node = (*Head)->next;

LinkList  temp_node_pre = (*Head);

if(NULL == (*Head))

{

printf("链表已空,已无元素可以删除\n");

return FALSE;

}

while(NULL != temp_node->next)

{

temp_node = temp_node->next;

temp_node_pre = temp_node_pre->next;

}

*x = temp_node->data;//保存最后一个结点的数据域的值

temp_node_pre->next = NULL;//修改原来结点的倒数第二个结点的指向

free(temp_node);//此时temp_node指向最后一个结点,防止内存泄露,释放最后一个结点的内存

return TRUE;

}

//带头结点的按照位置删除,首先要考虑删除的位置是否存在,然后由于带头结点,所以无论是一个元素,还是多个元素删除操作都一样

Status Delite_Head_Pos_SeqNode(LinkList Head,int pos, ElemType *x)

{

//定义临时变量防止头指针被修改  ,首先让指向首元结点,尾删用它来定位最后一个结点,

//由于单链表,只知道当前节点的下一个结点的位置,那么,就需要定义一个变量保存当前结点的地址

LinkList temp = Head->next;//定位最后一个指针需要删除的结点的前一个结点

LinkList temp_node_pre = Head;//保存即将删除的结点的地址

if(NULL == temp)

{

printf("链表已空,已经没有元素删除\n");

return FALSE;

}

if(pos > temp_node_pre->data )//判断删除的位置是否存在

{

printf("删除位置出错\n");

return FALSE;

}

for(int i = 0; i < pos-1;i++)//遍历链表找到链表要删除的结点的前一个结点

{

temp = temp->next;//定位要删除的位置

}

*x = temp->next->data;//用x返回需要删除结点的值

temp_node_pre = temp->next;

temp->next = temp->next->next;//让删除的前一个结点指向要删除的下一个结点

free(temp_node_pre);//此时temp_node指向删除的结点,防止空间泄露,释放内存

temp_node_pre = NULL;

Head->data--;//头结点保存着链表的长度,删除以后减去一个

return TRUE;

}

按照位置删除:同样也不在给出示意图:对比按照位置删除的图。

//不带头结点按照位置删除,由于当只有一个结点的删除需要修改头指针指向需要特别处理

Status Delite_No_Head_Pos_SeqNode(LinkList *Head, int pos, ElemType *x)

{

int NUM = 0;//统计一共链表一共多少个结点

//定义临时变量防止头指针被修改  ,首先让指向首元结点,尾删用它来定位最后一个结点,

//由于单链表,只知道当前节点的下一个结点的位置,那么,就需要定义一个变量保存当前结点的地址

LinkList temp = (*Head);

if(NULL == temp)

{

printf("链表已空,已无元素可以删除\n");

return FALSE;

}

else if(NULL == temp->next)//处理只有一个结点的情况

{

*x = temp->data;//用x返回需要删除结点的值

(*Head) = NULL;//头结点置空

free(temp);//释放第一个结点的空间,防止内存泄露

temp = NULL; //防止指向非法空间

return TRUE;

}

else

{

while(NULL != temp)

{

NUM++;

temp = temp->next;

}

if(pos > NUM)

{

printf("删除位置出错");

return FALSE;

}

if(0 == pos)

{

temp = *Head;

*x = temp->data;

*Head = temp->next;

free(temp);

temp = NULL;

}

else

{

//当遍历确定pos位置正确后,此时几个指针的指向已经发生改变,需要重新指向头指针,遍历找到需要插入的结点

temp = *Head;

for(int i = 0; i < pos-1; i++)

{

temp = temp->next;

}

*x = temp->next->data;//用x返回需要删除结点的值

LinkList free_node = temp->next;//free_node用与保存即将删除的结点

temp->next = temp->next->next;

free(free_node);//释放第一个结点的空间,防止内存泄露

free_node = NULL;

}

}

return TRUE;

}

打印输出所有数据:

//带头结点打印输出所有数据

Status Show_Node(LinkList Head)

{

LinkList temp = Head->next;

while(NULL != temp)

{

printf("%d  ",temp->data);

temp = temp->next;

}

printf("\n");

return TRUE;

}

//不带头结点打印输出所有数据

Status Show_Node_No_Head(LinkList Head)

{

LinkList temp = Head;

while(NULL != temp)

{

printf("%d  ",temp->data);

temp = temp->next;

}

printf("\n");

return TRUE;

}

清空链表:释放所有有效结点,对于带头结点链表,要把头结点的 数据域置0

//不带头结点的清空链表,释放所有的结点

Status Clean_No_Head_SeqNode(LinkList *Head )

{

LinkList temp_node = *Head;

LinkList temp_node_pre = *Head;

*Head = NULL;

while(NULL != temp_node)

{

temp_node = temp_node->next;//因为头结点不空,让临时指针指向下一个结点

free(temp_node_pre);//释放第一结点的空间

temp_node_pre = temp_node;//指向下一个空间

}

return TRUE;

}

//带头结点的清空链表,释放除头结点以外的空间

Status Clean_Head_SeqNode(LinkList Head)

{

LinkList temp_node = Head->next;

LinkList temp_node_pre = Head->next;

Head->next = NULL;

Head->data = 0;

while(NULL != temp_node->next)

{

temp_node = temp_node->next;

free(temp_node_pre);

temp_node_pre = temp_node;

}

return TRUE;

}

排序:对于排序,首先可以采用各种排序方法,然后就是排序过程中,我提一点就是只需要把值交换并不需要交换结点。这里采用冒泡排序。

//排序,由于只需要交换两个链表中的值就好了,不用修改指针指向所以带与不带头结点操作差不多,只是有细微的差别

Status Sort_No_Head_SeqNode(LinkList Head)

{

LinkList temp_node_i = Head;

LinkList temp_node_j = Head;

LinkList temp_node_pre = Head;

ElemType temp;//用与交换值

if(NULL == temp_node_i)

{

printf("链表为空,无法排序\n");

return FALSE;

}

for(temp_node_i = temp_node_i->next; NULL != temp_node_i; temp_node_i = temp_node_i->next)

{

for( temp_node_j = temp_node_j; NULL != temp_node_j;  temp_node_j = temp_node_j->next)

{

if(temp_node_j->data > temp_node_pre->data)

{

temp = temp_node_j->data;

temp_node_j->data = temp_node_pre->data;

temp_node_pre->data = temp;

temp_node_pre = temp_node_pre->next;

}

temp_node_pre = temp_node_pre->next;

}

}

return TRUE;

}

//排序带头结点

Status Sort_Head_SeqNode(LinkList Head)

{

LinkList temp_node_i = Head->next;

LinkList temp_node_j = Head->next;

LinkList temp_node_pre = Head->next;

ElemType temp;//用与交换值

if(NULL == temp_node_i)

{

printf("链表为空,无法排序\n");

return FALSE;

}

for(temp_node_i = temp_node_i; NULL != temp_node_i->next; temp_node_i = temp_node_i->next)

{

for(temp_node_j = Head; NULL != temp_node_j->next; temp_node_j = temp_node_j->next)

{

if(temp_node_j->data > temp_node_j->next->data)

{

temp = temp_node_j->next->data;

temp_node_j->next->data = temp_node_j->data;

temp_node_j->data = temp;

}

}

}

return TRUE;

}

以上对单链表带头结点与不带头结点的头插、尾插、按照位置插、头删、尾删、按照位置删除、清空链表、排序、打印输出做了尽可能详尽的注释,都做了测试,但是没有给出主函数,数据结构中,这里不是重点,并且限于篇幅,因而没有给出主函数。当然以上代码只是我的理解,大家可以如果发现那块有错误,希望指正。

最后,细心的大家会发现这里边有好多重发的代码:比如生成新结点,定位要插入删除的位置,有好多重复的代码,那么就可以把它写成一个函数,简化代码,更有,这种结构的结构体,操作起来有些不方便,如果采用下边这种结构,更会简化,并且便于理解。

typedef struct node  //节点类型

{

type value;

struct node *next;

}Node;

typedef struct list

{

Node *phead;

Node *ptail;

}List;

这里先不做解释,后边我会做出详细解释。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值