动态单链表的传统存储方式和10种常见操作-C语言实现
顺序线性表的优点:方便存取(随机的),特点是物理位置和逻辑为主都是连续的(相邻)。但是也有不足,比如;前面的插入和删除算法,需要移动大量元素,浪费时间,那么链式线性表 (简称链表) 就能解决这个问题。
一般链表的存储方法
一组物理位置任意的存储单元来存放线性表的数据元素,当然物理位置可以连续,也可以不连续,或者离散的分配到内存中的任意位置上都是可以的。故链表的逻辑顺序和物理顺序不一定一样。
因为,链表的逻辑关系和物理关系没有必然联系,那么表示数据元素之间的逻辑映象就要使用指针,每一个存储数据元素的逻辑单元叫结点(node)。
结点里有两个部分,前面存储数据元素内容,叫数据域,后面部分存储结点的直接后继在内存的物理位置,叫指针域。那么就可以实现用指针(也叫链)把若干个结点的存储映象链接为表,就是链式线性表。
上面开始介绍的是最简单的链表,因为结点里只有一个指针域,也就是俗称的单链表(也叫线性链表)。
单链表可以由一个叫头指针的东东唯一确定,这个指针指向了链表(也就是直接指向第一个结点)。因此单链表可以用头指针的名字来命名。且最后一个结点指针指向NULL。
链表类别
1、实现的角度:动态链表,静态链表(类似顺序表的动态和静态存储)
2、链接方式的角度:单链表,双向链表,循环链表
单链表
单链表的头结点
一般是使用单链表的头指针指向链表的第一个结点,有的人还这样做,在第一个结点之前再加一个结点(不保存任何数据信息,只保存第一个结点的地址,有时也保存一些表的附加信息,如表长等),叫头结点(头结点是头结点,第一个结点是第一个结点)。那么此时,头指针指向了头结点。并且有无头结点都是可以的。链表还是那个链表,只不过表达有差异。
那么问题来了!为什么还要使用头结点?
作用是对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。
描述动态单链表
有两种做法,一种是传统的动态链表结构,暨只定义结点的存储结构,使用4个字节的指针变量表示线性表。还有一种是直接采用结构体变量(类似顺序表)来表示线性表。
顾名思义,肯定是第二种方法比较方便。
先看传统存储动态单链表结构的操作
/************************************************************************/
/* 文件名称:ADT.h
/* 文件功能:动态单链表传统存储结构和常见操作
/* 作 者:dashuai
/* 备 注:以下算法,并没有考证是否全部都是最佳优化的,关于算法的优化后续研究
/************************************************************************/
#include <stdio.h>
#include <stdlib.h> //传统的单链表动态存储结构(带头指针)
typedef struct Node//Node标记可以省略,但如结构里声明了指向结构的指针,那么不能省略!
{
char data;//数据域
struct Node *next;//指针域
} Node, *LinkList;//LinkList是指向Node结构类型的指 /*
1、查找(按值)算法
找到第一个值等于value的结点,找到则返回存储位置
*/
LinkList getElemByValue(LinkList L, char value); /*
2、查找(按序号)算法
查找第i个元素,并用变量value返回值,否则返回提示,即使知道了序号,也必须使用指针顺次查访
*/
void getElemByNum(LinkList L, int num, char *value); /*
3、删除结点
删除元素,并用value返回值
*/
void deleteList(LinkList L, int i, char *value); /*
4、插入结点
在第i个位置之前插入结点e
*/
void insertList(LinkList L, int i, char value); /*
5、头插法建立单链表(逆序建表)
从一个空结点开始,逐个把 n 个结点插入到当前链表的表头
*/
void createListByHead(LinkList *L, int n); /*
6、尾插法建立单链表(顺序建表)
从一个空结点开始,逐个把 n 个结点插入到当前链表的表尾
*/
void createListByTail(LinkList *L, int n); /*
7、尾插法建立有序单链表(归并链表)
把两个有序(非递减)链表LA LB合并为一个新的有序(非递减)链表LC(空表)
要求:不需要额外申请结点空间来完成
*/
void mergeListsByTail(LinkList LA, LinkList LB, LinkList LC); /*
8、销毁单链表
*/
void destoryLinkList(LinkList *L); /*
9、求表长,长度保存到头结点
*/
void getLength(LinkList L); /*
10、遍历单链表
*/
void traversalList(LinkList L);
C变量的随用随定义,可以确定C99之后新增加的,和c++一样,貌似一些编译器还不支持
#include "ADT.h" /*
1、查找(按值)算法
找到第一个值等于value的结点,找到则返回存储位置
*/
LinkList getElemByValue(LinkList L, char value)
{
//定义指示指针指向L第一个结点
LinkList p = L->next;//L开始指向了头结点,L->next指向的就是第一个结点 while (p && p->data != value)
{
//p++;显然错误,离散存储,非随机结构
p = p->next;//指针顺链移动,直到找到或者结束循环
}
//当找不到,则p最终指向NULL,循环结束
return p;//返回存储位置或NULL
}
算法执行时间和value有关,时间复杂度为0(n),n表长
/*
2、查找(按序号)算法
查找第i个元素,并用变量value返回值,否则返回提示,即使知道了序号,也必须使用指针顺次查访
*/
void getElemByNum(LinkList L, int num, char *value)
{
int count = ;
LinkList p = L->next;
//控制指针p指向第num个结点
while (p && count < num)//num若为0或者负数直接跳出循环,若超出表长则遍历完毕,跳出循环,找到了元素也跳出循环
{
p = p->next;//p指向第num个结点,count此时为num值
count++;
}
//如果num大于表长,则count值增加到表长度时,p恰好指向表尾结点,遍历完整个链表也找不到结点,此时再循环一次
//p指向null,count = 表长 + 1,循环结束,这里也隐含说明了num大于 表长 的不合法情况
if (!p || count > num)//说明num至少比 表长 大
{
//num <= 0或者num大于表长时,跳出while循环,来到if语句,判断不合法
puts("num值非法!");
}
else
{
*value = p->data;
printf("找到第%d个元素的值 = %c\n", num, *value);
}
}
//时间复杂度0(n),n为表长
/*
3、删除结点
删除第i个元素,并用value返回值
*/ void deleteList(LinkList L, int i, char *value)
{
LinkList p = L;//头脑一定要清晰!这里p应该指向头结点
LinkList temp;
int j = ;//对应着头结点的序号0 /*while (p && j < i)
{
p = p->next;此时,p指向的是i元素位置,要删除i元素,需要知道i的前驱!
j++;
}*/ while (p->next && j < i - )//i - 1可以保证指向其前驱 ,j=0需要注意,删除是从1-n都可以
{
p = p->next;//指向i元素前驱(i-1)
j++;
}
//必须想到判断合法性
if (!(p->next) || j > i - )//同样判断i的下边界,和上边界
{
puts("i值不合法!");
}
else
{
//找到了前驱p
temp = p->next;//temp指针指向i元素
//p->next = p->next->next;等价于
p->next = temp->next;//链表删除结点的关键逻辑
*value = temp->data;
free(temp);//释放temp指向的结点的内存空间
temp = NULL;
puts("删除成功!");
}
}
时间复杂度,虽然没有移动任何元素,还是0(n),因为最坏时核心语句频度为n(表长)
为什么不是p?
//判断表为非空,因为不止一次删除操作!总会删空,则p还是指向的头结点!如果依然是while(p &&……),表空时,按道理函数不应该再执行核心语句,提前判断出错,但此时却还要执行循环体,循环结束才能到if(!p),而使用while(p->next && ……),表空就直接跳出循环,到if语句,提示错误。这是删除算法总是需要注意的细节,插入算法则是如果内存有限,或者是顺序的表,或者静态链表,那么总是要注意存储空间满足大小的问题
/*
4、插入结点
在第i个位置之前插入结点e,换句话说就是在第i-1个位置之后插入,类似前面的删除操作思想
*/
void insertList(LinkList L, int i, char value)
{
LinkList p = L;
LinkList s;
int j = ;
//和删除不同,插入操作不用注意表空的情况
while (p && j < i - )
{
p = p->next;
j++;
} if (!p || j > i - )//i小于1或者大于表长+1不合法
{
puts("i不合法!");
}
else
{
//先分配结点存储空间
s = (LinkList)malloc(sizeof(Node));
//依次传入插入的元素内容和修改逻辑链接
s->data = value;
//链表插入算法的关键逻辑
s->next = p->next;
p->next = s;//顺序不可颠倒,原则是指针在修改前,先保留再修改,不能先修改再保留
puts("插入成功!");
}
}
插入算法时间复杂度分析:0(n),最坏情况下频度是n
/插入和删除算法,还有一个思路,就是既然需要每次都找前驱,那么为什么不弄两个指针呢?一个指向当前位置,一个紧随其后指向前,个人其实感觉是脱裤子放屁……
//以删除为例
void deleteList(LinkList L, int i, char *value)
{
LinkList prePoint = L;//前驱指针初始化指向头结点
LinkList point = L->next;//当前指针初始化指向第一个结点
LinkList temp = NULL;
int j = ;
//i要>0,且小于等于表长
while (point && j < i)//如果表非空,找到要删除的元素位置
{
point = point->next;
prePoint = prePoint->next;//分别顺次后移
j++;
} if (!point || j > i)
{
puts("i不合法!");
}
else
{
temp = point;
prePoint->next = point->next;
*value = temp->data;
free(temp);
temp = NULL;
puts("删除成功!");
}
}
时间复杂度依然是O(n)
/*
5、头插法建立单链表(逆序建表)
从一个空结点开始,逐个把 n 个结点插入到当前链表的表头
*/ //开始就空表,则肯定先分配结点(带头结点)
void createListByHead(LinkList *L, int n)
{
LinkList p = NULL;
int i = ;
*L = (LinkList)malloc(sizeof(Node));//L指向头结点
(*L)->next = NULL;//空表建立
//头插法
for (i = ; i <= n; i++)
{
p = (LinkList)malloc(sizeof(Node));//先创建要插入的结点
scanf("%c", &(p->data));//给结点数据域赋值 再次验证 -> 优先级高于取地址 & while (getchar() != '\n')
{
continue;
} p->next = (*L)->next;//再让插入的结点的next指针指向后继(链接的过程),注意后继不能为空(除去第一次插入)
//p->next = NULL;//错误
(*L)->next = p;//最后保证插入结点是第一个结点,把头结点和第一个结点链接起来。
} printf("头插法建表成功\n");
}
时间复杂度:必然是O(n),插入了n个元素
链表和顺序表存储结构不同,动态,整个可用内存空间可以被多个链表共享,每个链表无需事先分配存储容量,由系统应要求自动申请。建立链表是动态的过程。
//如a b c d,依次头插法(头插 总是在第一个结点前插入,暨插入的结点总是作为第一个结点)到空链表里,那么完成之后是
//d c b a
下面是正序的尾插法,如图
/*
6、尾插法建立单链表(顺序建表)
//对 5 算法的改进
*/
//头插法算法简单,但生成的链表结点次序和输入的顺序相反。有时不太方便。
//若希望二者次序一致,可采用尾插法建表。该方法是将新结点顺次的插入到当前链表的表尾上,为此必须增加一个尾指针tail,
//使其始终指向当前链表的尾结点。
void createListByTail(LinkList *L, int n)
{
LinkList tail = NULL;//尾指针
int i = ;
LinkList p = NULL;//代表前驱
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
tail = *L; for (i = ; i <= n; i++)
{
p = (LinkList)malloc(sizeof(Node));
//_CRTIMP int __cdecl scanf(_In_z_ _Scanf_format_string_ const char * _Format, ...);返回int,传入的参数个数
/*如果被成功读入,返回值为参数个数
如果都未被成功读入,返回值为0
如果遇到错误或遇到end of file,返回值为EOF*/
scanf("%c", &(p->data));
//清空输入队列的剩余的所有字符
while (getchar() != '\n')
{
continue;
}
//尾插操作,后继总是空的
p->next = NULL;
//链接前驱
tail->next = p;//万万不能写成*L->next = p;
//保证尾指针tail总是指向最后一个结点
tail = p;
} printf("尾插法建表成功! \n");
}
这样操作,输入的序列和输出的序列是正序的,且时间复杂度为O(n)
/*
7、尾插法建立有序单链表(归并链表)
把两个有序(非递减)链表LA LB合并为一个新的有序(非递减)链表LC(空表)
要求:不需要额外申请结点空间来完成
*/ //1、比较数据域,保证有序
//2、尾插法思想
//3、不需要额外申请结点空间来完成!
//使用现有的内存空间,完成操作,那么可以想到用LC的头指针去指向其中某个表的头结点,内存共享 void mergeListsByTail(LinkList LA, LinkList LB, LinkList LC)
{
//因为要比较数据大小,需要两个标记指针,分别初始化为标记AB的第一个结点
LinkList listA = LA->next;
LinkList listB = LB->next;
//还要不开辟内存,那么内存共享,需要把表C让其他表去表示
LinkList listC;//声明一个标记C的指针
LC = LA;//比如表A。C表的头指针指向A表的头结点,做自己的头结点
listC = LA;//C表的标记指针需要初始化,指向A的头结点,待命
//接下来比较AB表数据,标记指针会顺次后移,早晚有一个先指向末尾之后的NULL,故判断是哪一个表的
while (listA && listB)
{
//判断数据大小,非递减
if (listA->data <= listB->data)
{
//则A的结点插入到C表(尾插),单链表不可使用头指针做遍历
listC->next = listA;//先把A的结点链到C表
listC = listA;//listC等价于尾指针
//A指针后移,继续循环比较
listA = listA->next;
}
else
{
//把B的结点插入到C(尾插)
listC->next = listB;
listC = listB;
listB = listB->next;
}//end of if
}//end of while
//循环结束,只需要把剩下的某个表的结点一次性链接到C表尾
if (listA)
{
//说明B空
listC->next = listA;
} if (listB)
{
//A空
listC->next = listB;
}
//最后AB表比较之前一定有一个表都被遍历了(也就是链接到了C),剩下的结点比如属于某个表的,最后也都链接到C尾部
//那么,此时就还有一个结点,那就是B表的头结点!勿忘把B表头结点释放,这才是完全的两个归并为一个
free(LB);
LB = NULL;//杜绝野指针
}
算法时间复杂度,和头插法比较的话,还是O(n),其实顺序表的有序归并也是这个时间复杂度O(A.length + B.length),但是链表的尾插法归并没有移动元素,只是解除和重建链接的操作,也没有额外开辟内存空间。空间复杂度不同。
/*
8、销毁单链表
*/
void destoryLinkList(LinkList *L)
{
LinkList p = NULL; while (*L)
{
p = (*L)->next;
free(*L);//free不能对指向NULL的指针使用多次!
*L = p;
//彻底搞掉指针本身,free(L)仅仅是销毁指针指向的内存,故还要连头结点一起干掉,不过while循环里隐形的包含了
} *L = NULL;
puts("链表L已经销毁,不存在!");
}
函数内部有动态分配内存的情形,应该把参数设定为指向指针的指针,当然还有别的方法,我习惯而已。
记得说:值传递函数,是把实参的一个拷贝送到函数体,函数体修改的是那份传入的拷贝,不是函数跑到main里去给它修改。
且形参和传入的拷贝,还有函数体内的变量(栈中分配的内存),都是是代码块内的自动存储类型的变量,也就是局部变量,函数执行完毕,变量自动销毁,改变就不起作用。
指针形参可以改变实参,但是如果是针对函数内动态分配了内存的情况,把堆分配的内存地址赋给了指针参数,改变的是指针指向的内容,而指针变量(形参)本身的内存地址没有改变,故根本句不会成功修改实参。
指向指针的指针,存放的是指向实参内存地址A的指针的地址B,修改B地址,改变了B指向的内容,而B指向的内容恰恰就是一级指针A本身,一级指针A的修改,使得实参被改变,对实参(指针变量C),需要取出指针C自己的的地址,传入函数。达到间接修改实参的目的。
/*
9、求表长,长度保存在头结点
*/
void getLength(LinkList L)
{
int length = ;
LinkList p = NULL; if (L)
{
p = L->next; while (p)
{
p = p->next;
length++;
} L->data = length;
printf("%d\n", L->data);
}
else
{
puts("表已经销毁!无法计算长度了……");
}
}
/*
10、遍历链表
*/
void traversalList(LinkList L)
{
int i = ;
int length = ;
LinkList p = L->next;
length = L->data;//遍历之前,务必先求表长!
puts("遍历之前,务必先求表长!"); for (; i < length; i++)
{
putchar(p->data);
p = p->next;
}
putchar('\n');
}
测试
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "ADT.h" int main(void)
{
LinkList L = NULL;//完全的空表,连头结点都没有
LinkList LTwo = NULL;
LinkList val = NULL;
int i = ;
char value = ''; puts("请输入5个字符变量(一行一个):");
puts("使用尾插法建立单链表L,5个结点,一个头结点");
//输入 12345
createListByTail(&L, );//尾插法建表 //12345正确的存入到了5个结点里,尾插法创建了一个单链表L //先求长度
puts("L长度为");
getLength(L); //遍历 12345
puts("遍历这个链表结点元素");
traversalList(L); //用完必须销毁
puts("把链表L销毁,L = NULL;");
destoryLinkList(&L); //头插法 abcde
//void createListByHead(<wo, 5);//报错;语法错误 : 缺少“;”(在“类型”的前面)
puts("使用头插法建立新的单链表LTwo,5个结点,一个头结点");
createListByHead(<wo, ); //求长度
getLength(LTwo); //遍历 edcba
puts("遍历表中结点元素");
traversalList(LTwo); //按值查找
puts("查找LTwo表的结点数据 = ‘2’的结点");
val = getElemByValue(LTwo, ''); if (val)
{
printf("找到了val,地址 = %p \n", &val);
} puts("‘2’在表 LTwo 里没找到!"); //插入结点
puts("在位置 1 之后插入一个结点,里面数据是 ‘p’");
insertList(LTwo, , 'p'); //遍历 pedcba
puts("开始遍历表LTwo");
traversalList(LTwo); //按序查找
puts("查找位置=2的结点,并打印出它的数据内容");
getElemByNum(LTwo, , &value); //删除结点
puts("删除位置 1 的结点,并打印出删除结点的数据");
deleteList(LTwo, , &value);
printf("%c\n", value); //遍历 pedcba
puts("再次遍历链表LTwo");
traversalList(LTwo); //求链表长度,把长度保存的头结点
puts("计算链表长度,并把长度保存到了LTwo的头结点");
getLength(LTwo);
printf("%d\n", LTwo->data); //必须有销毁
puts("动态存储的结构用完一定要销毁");
destoryLinkList(<wo); //此时销毁的表长规定是0
puts("销毁之后,链表长度:");
getLength(LTwo); system("pause");
return ;
}
scanf函数的特点是接受单词,而不是字符串,字符串一般是gets函数,单个字符接收是getchar函数,因为scanf函数遇到空白字符(tab,空格,回车,制表符等)就不再读取输入,那字符串怎么能方便输入?
但是输入队列里如果还有字符,那么会留到缓存内,需要在定义里使用getchar函数来消除回车带来的影响。
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!
动态单链表的传统存储方式和10种常见操作-C语言实现的更多相关文章
- 数据结构 - 动态单链表的实行(C语言)
动态单链表的实现 1 单链表存储结构代码描述 若链表没有头结点,则头指针是指向第一个结点的指针. 若链表有头结点,则头指针是指向头结点的指针. 空链表的示意图: 带有头结点的单链表: 不带头结点的单链 ...
- 有一个线性表,采用带头结点的单链表L来存储,设计一个算法将其逆置,且不能建立新节点,只能通过表中已有的节点的重新组合来完成。
有一个线性表,采用带头结点的单链表L来存储,设计一个算法将其逆置,且不能建立新节点,只能通过表中已有的节点的重新组合来完成. 分析:线性表中关于逆序的问题,就是用建立链表的头插法.而本题要求不能建立新 ...
- Linux C++ 单链表添加,删除,输出,逆序操作
/*单链表操作*/#include <iostream>using namespace std; class Node{ public: Node(){ next=0; } Node(in ...
- 人工智能改进传统云ERP的10种方法
http://blog.itpub.net/31542119/viewspace-2168809/ 随着数字化转型的进程加快,企业开始重新评估ERP的作用.传统ERP经过多年僵硬化定制过于追求生产的一 ...
- 合并两个以单链表形式表示的关于x的多项式(基于c语言)
只写函数内部的,不懂得可以看前面一篇文章对链表的实现: pLinklist addBothLinklist(Linklist* first,Linklist* second){ Linklist *n ...
- 线性表->链式存储->线形链表(单链表)
文字描述: 为了表示前后两个数据元素的逻辑关系,对于每个数据元素,除了存储其本身的信息之外(数据域),还需存储一个指示其直接后继的信息(即直接后继的存储位置,指针域). 示意图: 算法分析: 在单链表 ...
- 一起talk C栗子吧(第十二回:C语言实例--单链表一)
各位看官们,大家好.从今天開始,我们讲大型章回体科技小说 :C栗子.也就是C语言实例.闲话休提, 言归正转. 让我们一起talk C栗子吧! 看官们,上一回中咱们没有说详细的样例,并且是说了样例中的文 ...
- 单链表 C语言 学习记录
概念 链接方式存储 链接方式存储的线性表简称为链表(Linked List). 链表的具体存储表示为: 用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是不连续的). 链表中 ...
- VS环境下基于C++的单链表实现
------------恢复内容开始------------ #include<iostream> using namespace::std; typedef int ElemType; ...
随机推荐
- Go+sublime text3的环境搭建
1.安装Go语言. .msi下载地址:http://download.csdn.net/detail/u014075041/9555543 根据提示安装成功! 在命令行中执行 go env 有提示 ...
- Deploying JRE (Native Plug-in) for Windows Clients in Oracle E-Business Suite Release 12 (文档 ID 393931.1)
In This Document Section 1: Overview Section 2: Pre-Upgrade Steps Section 3: Upgrade and Configurati ...
- [MySQL][Spider][VP]Spider-3.1 VP-1.0 发布
我很高兴的宣布 Spider 存储引擎 3.1 Beta 版本和垂直分区存储引擎 1.0 Beta 版本发布了. Spider 是数据库拆分的存储引擎: http://spiderformysql.c ...
- 双击防止网页放大缩小HTML5
幕双击放大或缩小.即相当于这样设置 <meta name="viewport" content="width=device-width, initial-scale ...
- 关于CAP定理的个人理解
CAP定理简介 在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致性(C ...
- Java多线程10:ThreadLocal的作用及使用
ThreadLocal的作用 从上一篇对于ThreadLocal的分析来看,可以得出结论:ThreadLocal不是用来解决共享对象的多线程访问问题的,通过ThreadLocal的set()方法设置到 ...
- Ubuntu Desktop开发生产环境搭建
Ubuntu Desktop开发生产环境搭建 1 开发生产环境搭建 在本节内容开始前,先定义一下使用场合,没有哪种系统或者设备是万能的,都有它的优点和缺点,能够在具体的使用场景,根据自身的需求来取 ...
- 【读书笔记】Programming Entity Framework CodeFirst -- 初步认识
以下是书<Programming Entity Framework Code First>的学习整理,主要是一个整体梳理. 一.模型属性映射约定 1.通过 System.Component ...
- 新版markdown功能发布!支持github flavored markdown!
让大家久等了!新版markdown功能一直拖到今天才发布,很是愧疚...但不管怎么样,总算发布了! 今年1月份发布第一版markdown功能之后,很多园友反馈说做得很烂,我们综合大家的反馈之后发现不仅 ...
- SQL—大话函数依赖与范式
说明:数据库中的某些概念真的很让人头疼,概念的东西本来就是很枯燥的,再加上枯燥的学习,那就更加枯燥了.概念这东西,你不理解也能生产东西,经验多了就行,但是为了更深入的学习,你还必须理解.这里,我抛开书 ...