数据结构-线性表的链式存储相关算法(C语言实现)
链表的简单介绍
为什么需要线性链表
当然是为了克服顺序表的缺点,在顺序表中,做插入和删除操作时,需要大量的移动元素,导致效率下降。
线性链表的分类
- 按照链接方式:单链表、循环链表、双链表
- 按照实现角度:静态链表、动态链表
线性链表的创建和简单遍历
算法思想
创建一个链表,并对链表的数据进行简单的遍历输出。
算法实现
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;//数据域
struct Node * pNext;//指针域 ,通过指针域 可以指下一个节点 “整体”,而不是一部分;指针指向的是和他本身数据类型一模一样的数据,从结构体的层面上说,也就是说单个指向整体,(这里这是通俗的说法,实施情况并非是这样的)下面用代码进行说明。
}NODE,*PNODE; //NODE == struct Node;PNODE ==struct Node *
PNODE create_list(void)//对于在链表,确定一个链表我们只需要找到“头指针”的地址就好,然后就可以确认链表,所以我们直接让他返回头指针的地址
{
int len;//存放有效节点的个数
int i;
int val; //用来临时存放用书输入的节点的的值
PNODE pHead = (PNODE)malloc(sizeof(NODE)); //请求系统分配一个NODE大小的空间
if (NULL == pHead)//如果指针指向为空,则动态内存分配失败,因为在一个链表中首节点和尾节点后面都是NULL,没有其他元素
{
printf("分配内存失败,程序终止");
exit(-1);
}
PNODE pTail = pHead;//声明一个尾指针,并进行初始化指向头节点
pTail->data = NULL;//把尾指针的数据域清空,毕竟和是个结点(清空的话更符合指针的的逻辑,但是不清空也没有问题)
printf("请您输入要生成链表节点的个数:len =");
scanf("%d",&len);
for (i=0;i < len;i++)
{
printf("请输入第%d个节点的值",i+1);
scanf("%d",&val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));//创建新节点,使之指针都指向每一个节点(循环了len次)
if(NULL == pNew)//如果指针指向为空,则动态内存分配失败,pNew 的数据类型是PNODE类型,也就是指针类型,指针指向的就是地址,如果地址指向的 //的 地址为空,换句话说,相当于只有头指针,或者是只有尾指针,尾指针应该是不能的,因为一开始的链表是只有一个 //头指针的,所以说,如果pNew指向为空的话,说明,内存并没有进行分配,这个链表仍然是只有一个头节点的空链表。
{
printf("内存分配失败,程序终止运行!\n");
exit(-1);
}
pNew->data = val; //把有效数据存入pNEW
pTail->pNext = pNew; //把pNew 挂在pTail的后面(也就是pTail指针域指向,依次串起来)
pNew->pNext = NULL;//把pNew的指针域清空
pTail = pNew; //在把pNew赋值给pTai,这样就能循环,实现依次连接(而我们想的是只是把第一个节点挂在头节点上,后面的依次进行,即把第二个
//节点挂在第一个节点的指针域上),这个地方也是前面说的,要给pHead 一个“别名的原因”
/*
如果不是这样的话,代码是这样写的:
pNew->data = val;//一个临时的节点
pHead->pNext = pNew;//把pNew挂到pHead上
pNew->pNext=NULL; //这个临时的节点最末尾是空
注释掉的这行代码是有问题的,上面注释掉的代码的含义是分别把头节点后面的节点都挂在头节点上,
导致头节点后面的节点的指针域丢失(不存在指向),而我们想的是只是把第一个节点挂在头节点上,后面的依次进行,即把第二个
节点挂在第一个节点的指针域上,依次类推,很明显上面所注释掉的代码是实现不了这个功能的,pTail 在这里的做用就相当于一个中转站的作用,类似于两个数交换算法中的那个中间变量的作用,在一个链表中pHead 是头节点,这个在一个链表中是只有一个的,但是如果把这个节点所具备的属性赋值给另外的一个变量(pTail)这样的话,pTail 就相当于另外的一个头指针,然后当然也是可以循环。
*/
}
return pHead;//返回头节点的地址
}
void traverse_list(PNODE pHead)//怎样遍历,是不能像以前一样用数组的,以为数组是连续的,这里不连续
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;//等价于 struct Node * pHead = NULL;把首节点的地址赋值给pHead(在一个链表中首节点和尾节点后面都是NULL,没有其他元素)
//PNODE 等价于struct Node *
pHead = create_list();
traverse_list(pHead);
return 0;
}
运行演示
算法小结
这只是一个简单的示例,其中用到的插入节点的算法就是尾插法,下面有具体的算法。
线性链表头插法实现
算法思想
从一个空表开始,每次读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前表的表头结点之后。
算法实现
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
}NODE,*PNODE;
//遍历
void traverse_list(PNODE pHead)//怎样遍历,是不能像以前一样用数组的,以为数组是连续的,这里不连续
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的长度\n");
int n;
int val;
scanf("%d",&n);
for (int i = n;i > 0;i--)
{
printf("请输入的第%d个数据",i);
PNODE p = (PNODE)malloc(sizeof(NODE));//建立新的结点p
if(NULL == p)
{
printf("内存分配失败,程序终止运行!\n");
exit(-1);
}
scanf("%d",&val);
p->data = val;
p->pNext = pHead->pNext;//将p结点插入到表头,这里把头节点的指针赋给了p结点
//此时,可以理解为已经把p节点和头节点连起来了,头指针指向,也就变成了
//p节点的指针指向了(此时的p节点相当于首节点了)
pHead->pNext = p;
}
return pHead;
}
int main(void)
{
PNODE pHead = NULL;
pHead = create_list();
traverse_list(pHead);
return 0;
}
运行演示
算法小结
采用头插法得到的单链表的逻辑顺序与输入元素顺序相反,所以也称头插法为逆序建表法。为什么是逆序的呢,因为在开始建表的时候,所谓头插法,就是新建一个结点,然后链接在头节点的后面,也就是说,最晚插入的结点,离头节点的距离也就是越近!这个算法的关键是 p->data = val;p->pNext = pHead->pNext; pHead->pNext = p;
。用图来表示的话可能更加清晰一些。
线性链表尾插法实现
算法思想
头插法建立链表虽然算法简单,但生成的链表中节点的次序和输入顺序相反,如果希望二者的顺序一致,可以采用尾插法,为此需要增加一个尾指针r,使之指向单链表的表的表尾。
算法实现
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的长度:\n");
int n;
int val;
PNODE r = pHead;//r 指针动态指向链表的当前表尾,以便于做尾插入,其初始值指向头节点,
//这里可以总结出一个很重要的知识点,如果都是指针类型的数据,“=”可以以理解为指向。
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("请输入的第%d个数据",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("内存分配失败,程序终止运行!");
exit(-1);
}
scanf("%d",&val);
p->data = val; //给新节点p的数据域赋值
r->pNext = p;//因为一开始尾指针r是指向头节点的, 这里又是尾指针指向s
// 所以,节点p已经链接在了头节点的后面了
p->pNext = NULL; //把新节点的指针域清空 ,先清空可以保证最后一个的节点的指针域为空
r = p; // r始终指向单链表的表尾,这样就实现了一个接一个的插入
}
return pHead;
}
//遍历
void traverse_list(PNODE pHead)//怎样遍历,是不能像以前一样用数组的,以为数组是连续的,这里不连续
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
//删除
void DelList(PNODE pHead,int i,int * e)
//在带头节点的单链表L中删除第i个元素,并将删除的元素保存到变量 *e 中
{
NODE * pre;
NODE * r;
int k = 0;
pre = pHead;
while (pre->pNext!=NULL && k<i-1)
//寻找被删除结点i的前驱节点i-1,使p指向它
{
pre = pre->pNext;
k = k+1;
}
if(pre->pNext == NULL)
{
printf("删除位置i不合理!");
exit(-1);
}
r = pre->pNext;
pre->pNext = r->pNext;//修改指针,删除结点
*e = r->data;
printf("您要删除的结点%d已经被删除!",*e);
free(r);//注意顺序,是最后才把rfree掉!
}
int main(void)
{
int val;
PNODE pHead = NULL;
pHead = create_list();
traverse_list(pHead);
DelList(pHead,2,&val);
traverse_list(pHead);
return 0;
}
运行演示
算法小结
通过尾插法的学习,进一步加深了对链表的理解,“=”可以理解为赋值号,也可以理解为“指向”,两者灵活运用,可以更好的理解链表中的相关内容。
还有,这个尾差法其实就是这篇文章中的一开始那个小例子中使用的方法。两者可以比较学习。
查找第i个节点(找到后返回此个节点的指针)
按序号查找
算法思想
在单链表中,由于每个结点 的存储位置都放在其前一个节点的next域中,所以即使知道被访问的节点的序号,也不能想顺序表中那样直接按照序号访问一维数组中的相应元素,实现随机存取,而只能从链表的头指针触发,顺链域next,逐个结点往下搜索,直到搜索到第i个结点为止。
要查找带头节点的单链表中第i个节点,则需要从**单链表的头指针L出发,从头节点(pHead->next)开始顺着链表扫描,用指针p指向当前扫面到的节点,初始值指向头节点,用j做计数器,累计当前扫描过的节点数(初始值为0).当i==j时,指针p所指向的节点就是要找的节点。
代码实现
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的长度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("请输入的第%d个数据",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("内存分配失败,程序终止运行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找第i个节点
NODE * getID(PNODE pHead,int i)//找到后返还该节点的地址,只需要需要头节点和要找的节点的序号
{
int j; //计数,扫描的次数
NODE * p;
if(i<=0)
return 0;
p = pHead;
j = 0;
while ((p->pNext!=NULL)&&(j<i))
{
p = p->pNext;
j++;
}
if(i==j)//找到了第i个节点
return p;
else
return 0;
}
//遍历
void traverse_list(PNODE pHead)//怎样遍历,是不能像以前一样用数组的,以为数组是连续的,这里不连续
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;
int n;
NODE * flag;
pHead = create_list();
traverse_list(pHead);
printf("请输入你要查找的结点的序列:");
scanf("%d",&n);
flag = getID(pHead,n);
if(flag != 0)
printf("找到了!");
else
printf("没找到!") ;
return 0;
}
运行演示
按值查找
算法思想
按值查找是指在单链表中查找是否有值等于val的结点,在查找的过程中从单链表的的头指针指向的头节点开始出发,顺着链逐个将结点的值和给定的val做比较,返回结果。
代码实现
# include <stdio.h>
# include <stdlib.h>
#include <cstdlib> //为了总是出现null未定义的错误提示
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的长度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("请输入的第%d个数据",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("内存分配失败,程序终止运行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找按照数值
NODE * getKey(PNODE pHead,int key)
{
NODE * p;
p = pHead->pNext;
while(p!=NULL)
{
if(p->data != key)
{
p = p->pNext;//这个地方要处理一下,要不然找不到的话就指向了系统的的别的地方了emmm
if(p->pNext == NULL)
{
printf("对不起,没要找到你要查询的节点的数据!");
return p;//这样的话,如果找不到的话就可以退出循环了,而不是一直去指。。。。造成指向了系统内存emmm
}
}
else
break;
}
printf("您找的%d找到了!",p->data) ;
return p;
}
//遍历
void traverse_list(PNODE pHead)//怎样遍历,是不能像以前一样用数组的,以为数组是连续的,这里不连续
{
PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ", p->data);
p = p->pNext;
}
printf("\n");
}
int main(void)
{
PNODE pHead = NULL;
int val;
pHead = create_list();
traverse_list(pHead);
printf("请输入你要查找的结点的值:");
scanf("%d",&val);
getKey(pHead,val);
return 0;
}
运行演示
算法小结
两个算法都是差不多的,第一个按序号查找,定义了一个计数变量j,它有两个作用,第一个作用是记录节点的序号,第二个作用是限制指针指向的范围,防止出现指针指向别的地方。第二个按值查找,当然也可以用相同的方法来限制范围,防止指针指向别的位置。或者和上面写的那样,加一个判断,如果到了表尾,为空了,就退出循环。
求链表的长度
算法思想
采用“数”结点的东方法求出带头结点单链表的长度。即从“头”开始“数”(p=L->next),用指针p依次指向各个节点。并设计计数器j,一直疏导最后一个节点(p->next == NUll),从而得到单链表的长度。
算法实现
# include <stdio.h>
# include <stdlib.h>
#include <cstdlib> //为了总是出现null未定义的错误提示
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的节点(输入0结束):\n");
int val=1;//赋一个初始值,防止 因为垃圾值而报错,下面都会被scanf函数给覆盖掉
PNODE r = pHead;
while(val != 0)
{
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("内存分配失败,程序终止运行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//计算链表的长度
int ListLength(PNODE pHead)
{
NODE * p;
int j;//计数
p = pHead->pNext;
j = 0;
while(p!=NULL)
{
p = p->pNext;
j++;
}
return j;
}
int main(void)
{
PNODE pHead = NULL;
pHead = create_list();
int len = ListLength(pHead);
printf("%d",len);
return 0;
}
运行演示
算法小结
在顺序表中,线性表的长度是它的属性,数组定义时就已经确定,在单链表中,整个链表由“头指针”来表示,单链表的长度在头到尾遍历的过程中统计计数,得到长度值未显示保存。
求单链表中的最大值以及实现就地址逆置链表
算法思想(求单链表的最大值)
通过两个指针就可以很简单实现这个问题,值得注意的是,要主要模拟比较的过程,通过指针指向的两个节点的数据域进行比较,把数据域大的节点的地址返回,如果找不到一直找。
算法实现
# include <stdio.h>
# include <stdlib.h>
typedef struct Node
{
int data;
struct Node * pNext;
} NODE,*PNODE;
PNODE create_list(void)
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
pHead->pNext = NULL;
printf("请输入要生成的链表的长度:\n");
int n;
int val;
PNODE r = pHead;
scanf("%d",&n);
for(int i = 0;i < n;i++)
{
printf("请输入的第%d个数据",i+1);
PNODE p = (PNODE)malloc(sizeof(NODE));
if(NULL == p)
{
printf("内存分配失败,程序终止运行!");
exit(-1);
}
scanf("%d",&val);
p->data = val;
r->pNext = p;
p->pNext = NULL;
r = p;
}
return pHead;
}
//查找单链表中的最大值
NODE * SearchMAx(PNODE pHead)
{
NODE * p1;//定义两个指针,依次比较两次的数据域大小
NODE * p2;
p1 = pHead->pNext;
p2 = p1->pNext;
while(p2 != NULL)
{
if(p2->data > p1->data) //注意这里和上面的赋值的顺序
{
p1 = p2;//把p1定义为最大结点的指针
p2 = p2->pNext;//继续走链表
}
else
{
p2 = p2->pNext; //如果一直没有找到比p1大的数据,继续找
}
}
return p1;//把最大的节点的地址返回
}
int main(void)
{
PNODE pHead = NULL;
NODE * p;
pHead = create_list();
p = SearchMAx(pHead);
printf("链表中的最大值为%d",p->data);
return 0;
}
运行演示
算法思想(就地址逆置换)
算法思想:逆置链表初始为空,表中节点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中),使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空。利用头插法。
算法实现
void converse(LinkList *head)
{
LinkList *p,*q;
p=head->next;
head->next=NULL;
while(p)
{
/*向后挪动一个位置*/
q=p;
p=p->next;
/*头插*/
q->next=head->next;
head->next=q;
}
}
参考文献
- 数据结构-用C语言描述(第二版)[耿国华]
- 数据结构(C语言版)[严蔚敏,吴伟民]
数据结构-线性表的链式存储相关算法(C语言实现)的更多相关文章
- C++线性表的链式存储结构
C++实现线性表的链式存储结构: 为了解决顺序存储不足:用线性表另外一种结构-链式存储.在顺序存储结构(数组描述)中,元素的地址是由数学公式决定的,而在链式储存结构中,元素的地址是随机分布的,每个元素 ...
- C++编程练习(2)----“实现简单的线性表的链式存储结构“
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素. 对于查找操作,单链表的时间复杂度为O(n). 对于插入和删除操作,单链表在确定位置后,插入和删除时间仅为O(1). 单链表不需要分配存储 ...
- C 线性表的链式存储实现及插入、删除等操作示例
一.链式存储的优势 线性表的存储可以通过顺序存储或链式存储实现,其中顺序存储基于数组实现(见本人上一篇博客),在进行插入删除等操作时,需对表内某一部分元素逐个移动,效率较低.而链式结构不依赖于地址连续 ...
- 线性表 顺序存储 链式存储 ---java实现
首先抽象出一个线性表抽象类(包括主要的增删操作) public abstract class MyAbstractList<E> { public abstract void add(E ...
- 线性表的链式存储——C语言实现
SeqList.h #ifndef _WBM_LIST_H_ #define _WBM_LIST_H_ typedef void List; typedef void ListNode; //创建并且 ...
- typedef struct LNode命名结构指针(线性表的链式存储)
一.typedef 关键字 1. 简介: typedef工具是一个高级数据特性,利用typedef可以为某一些类型自定义名称. 2. 工作原理: 例如我们定义链表的存储结构时,需要定义结点的存储数据元 ...
- 线性表的链式存储C语言版
#include <stdio.h> #include <malloc.h> #define N 10 typedef struct Node { int data; stru ...
- 线性表的链式存储结构的实现及其应用(C/C++实现)
存档----------- #include <iostream.h> typedef char ElemType; #include "LinkList.h" voi ...
- javascript实现数据结构:线性表--线性链表(链式存储结构)
上一节中, 线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中任一元素,它的存储位置可用一个简单,直观的公式来表示.然后,另一方面来看,这个特点也造成这种存储 ...
随机推荐
- web前端UI框架
分类:WEB前端 时间:2016年1月13日 目前,众多互联网公司APP都嵌入了大量的HTML5,移动端的开发越来越重视,HTML5的运用场景也越来越多了.在移动WEB开发的过程中,使用合适的移动WE ...
- 文件导出也可以这么写【js+blob】
文件导出在软件开发中是个比较常用的功能,基本原理也很简单: 浏览器向后台发送一个Get请求 后台处理程序接收到请求后,经过处理,返回二进制文件流 浏览器接收到二进制文件流后提示下载文件 调用的js方法 ...
- DaTaX当成jar包当作第三方库启动的相关问题
上一篇已经大致的将了本地状况下DaTaX的纯Java代码启动的过程 http://www.cnblogs.com/blogsofmy/p/8287637.html不了解的请点超链接 这次我们来说说文件 ...
- ubuntu系统备份与恢复(也适用于其他linux系统)
在windows环境下面,有很多的备份恢复软件,系统也自带了备份恢复功能,比较知名的软件比如ghost等,为什么要备份系统,我之前一直都是系统出现问题就全新安装原版的windows,然后各种设置,各种 ...
- 支持Touch ID!EOS 项目进展速报
Daniel Larimer 周六在 Steemit 上向大家介绍了 EOS 项目的最新进展,惊喜不少. 原文链接 : https://steemit.com/eosio/@dan/eos-io-de ...
- MongoDb进阶实践之二 如何在Windows上配置MongoDB
一.引言 上一篇文章,我介绍了如何在Linux系统上安装和配置MongoDB,其实都不是很难,不需要安装和编译,省去了Make && Make Install 命 ...
- 笔记:Hibernate SQL 查询
Hibernate 支持使用原生的SQL查询,使用原生SQL查询可以利用某些数据库特性,原生SQL查询也支持将SQL语句放在配置文件中配置,从而提高程序的解耦,命名SQL查询还可以用于调用存储过程. ...
- Java基础_Java概述
Java_概述 特点: 平台的移植性 开源 面向对象 多线程 安全性 工作方式: 先编译再解释执行. 先通过Javac命令将Java源代码编译成字节码文件(bytecode,类文件,.class,中间 ...
- 打印机驱动冲突和端口异常:win10更新部分补丁后,打印机本地连接(连接打印机的主机)可以打印,其他共享网络中的电脑可以连接到打印机,但不能打印——解决方案
一.问题描述: 1.A(WIN10系统)表示连接打印机的电脑,P表示打印机(型号:惠普127M),B(WIN7系统)表示局域网中的电脑 2.A升级后部分补丁后,A可以使用打印机P打印文件,B显示可以连 ...
- python自学日志--基础篇(1)
从认识python,到学习python,中间经历了挺长一段时间的心理挣扎.人总是对未知的事物有着天生的恐惧感,但是,人又是对未知充斥好奇.所以在最后,还是推开了这扇门,开始学习python. pyth ...