用C语言描述数据结构_线性表_单链表

版权声明:如需转载,请注明出处 https://blog.csdn.net/qq_36260974/article/details/84182965

- 什么是线性表
线性表是由n个元素(结点)组成的有限序列。n为线性表的长度,n=0是称为空表。

- 线性表的逻辑特征
(1)对于非空的线性表,有且有一个开始结点,它没有直接前驱,而仅有一个直接后继。
(2)对于非空的线性表,有且有一个终端结点,它没有直接后继,而仅有一个直接前驱。
(3)对于非空的线性表,内部结点(除去开始结点和终端结点)都有且仅有一个直接前驱和一个直接后继。

- 图示
在这里插入图片描述

- 什么是单链表
单链表简单的说就是顺序表的链式存储,在顺序表中,我们是用一组地址连续的存储单元来依次存放线性表的结点,因此结点的逻辑次序和物理次序一致。而链表则不然,链表是用一组任意的存储单元来存放线性表的结点,这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任何位置上。因此,链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息,这个信息称为指针( pointer)或链(link)。这两部分信息组成了链表中的结点结构:
在这里插入图片描述
其中,data域是数据域,用来存放结点的值;next域是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。链表正是通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。由于上述链表的每个结点只有一个链域,故将这种链表称为单链表( Single LinkedList)。
图示:
在这里插入图片描述
单链表的描述:

typedef char datatype;
typedef struct node
{
	datatype data;
	struct node *next;
}LinkList;
LinkList *head, *p;

值得一提的是,我们一定要严格区分指针变量和结点变量这两个概念。例如,以上定义的变量p是类型为 Linklist的指针变量,若p的值非空(p!=NULL),则它的值是类型为node的某一个结点的地址。通常p所指的结点变量并非在变量说明部分明显地定义,而是在程序执行过程中,当需要时才产生,故称为动态变量。实际上,它是通过标准函数生成的,即

p = malloc(sizeof(linklist));

函数 malloc 分配一个类型为 node 的结点变量的空间,并将其地址放入指针变量 p 中。一旦 p 所指的结点变量不再需要了,又可通过标准函数

free(p)

释放p所指的结点变量空间。因此,我们无法通过预先定义的标识符去访问这种动态的结点变量,而只能通过指针p 来访问它,即用 *p 作为该结点变量的名字来访问。由于结点类型node是结构类型,因而p是结构名,故可用加上“.”来取该结构的两个分量(*p),data和§.next。这种表示形式总是要使用圆括号,显得很不精练。因此,在C语言中,对指针所指结构体的成员进行访问时,通常用运算符“一>”(减号和大于号)来表示。例如取上面结构中的两个分量,可以写成pー>data和pー>next。它与前一种表示法的意义完全相同。

单链表上的基本运算
在这里插入图片描述

1、建立链表
(1)头插法建表
过程描述:头插法用到两个指针,首先产生第一个结点,将取到的字符存入该结点的数据域,然后指针p率先指向该结点,指针head紧跟着也指向该结点,意味着将该结点作为头结点,这样对第一个结点的操作就完成了;然后又产生第二个结点,指针p总是率先指向新产生的结点,而指针head也每次紧跟指针p的步伐,随其后即刻指向新产生的结点,这个过程表现了每次都将新产生的结点作为头结点的过程。按照该流程循环,直到在缓冲区当中读取到指定的结束字符,停止循环,也就不再产生新的结点,头插法建表完成,一条单链表产生了。
值得一提的是,由于每次新生结点都是链接到头结点上的,所以会造成输入数据的顺序和链表的遍历顺序相反的现象。
图示:
在这里插入图片描述

代码演示:

printf("\n请输入一串数据,以‘#’结束!\n");
ch = getchar();
while (ch != '#')
{
	p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
	p->data = ch;	
	p->next = head;
	head = p;
	ch = getchar();
}

头插法建表结果图:
在这里插入图片描述

(2)尾插法建表
过程描述:与头插法相比,尾插法用到三个指针,是因为头指针只用于指向头结点(第一个产生的结点)。然后如图所示的p结点用于即刻指向新产生结点,s结点用于将新产生的结点连到链表上。值得一说的是,上述算法当中,需要对产生的第一个结点(头结点)做特殊处理,为此用到第一个if语句,如果是第一个产生的结点,我们需要将其设为头结点,而对之后产生的结点我们只需要将它链接到链表上即可。对于第二个if语句而言,其作用是为了分别处理空表和非空表这两种情况,若读入的第一个字符就是结束符,则链表是空表。尾指针s也为空,结点s不存在,也就不能对其进行置空操作。否则链表head非空,最后一个结点s是终端结点,应将其指针域置空。
图示:
在这里插入图片描述

代码演示:

printf("\n请输入一串数据,以‘#’结束!\n");
ch = getchar();
while (ch != '#')
{
	p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
	p->data = ch;
	if(head == NULL)
	{
		head = p;
	}
	else
	{
		s->next = p;
	}
	s = p;
	ch = getchar();
}
if(s != NULL)
{
	s->next = NULL;
}

但是如果我们一开始就将头结点指定(空),是不是就避免了对头结点的特殊处理。同时,无论链表是否为空,头指针是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就统一了,也就没有上述方法的两个if语句了,如下图所示:
在这里插入图片描述
代码演示:

head = (LinkList *)malloc(sizeof(LinkList));
s = head;
printf("\n请输入一串数据,以‘#’结束!");
ch = getchar();
while (ch != '#')
{
	p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
	p->data = ch;
	s->next = p;
	s = p;
	ch = getchar();
}
s->next = NULL;

尾插法建表结果图:
在这里插入图片描述
2、序号查找
序号查找是按照位置来查找的,每次查找操作均需要从头结点开始遍历链表,过程当中对链表结点进行计数累加,当累加到目标位置的时候,输出该位置所存储的数据,即为找到目标结点;如果在遍历完成以后计数器也未累加到目标位置,则为未能找到目标位置,也就是说目标位置大于链表总长度。
代码演示:

printf("\n\t\t请输入查找位置:");
scanf("%d", &i);	
while (p != NULL)
{
	if (i == j)
	{
		printf("\n\t\t发现所查找位置结点数据为:%c\n", p->data);
		return;
	}	
	p = p->next;
	j++;		
}
printf("\n\t\t未发现所查找位置!\n");

序号查找结果图(头插法建表):
在这里插入图片描述
3、按值查找
按值查找是按照链表当中每个结点所存储的数据的值来进行查找的,每次查找也需要从头结点开始遍历链表,过程当中依次对比结点数据域所存储数据是否和目标数据相等,如若相等,则判为找到目标数据(注意:只能找到第一次出现的相同数据),若链表遍历完成以后,也没有找到目标数据,则判为未找到目标数据。也就是说,该链表当中不存在目标数据。
代码演示:

printf("\n\t\t请输入查找的结点值:");
scanf("%s",&key);
while (p != NULL)
{
	if(p->data == key)
	{
		printf("\n\t\t发现所查找结点!\t小标为:%d\n", j);
		return;
	}
	p = p->next;
	j++;
}
printf("\n\t\t未发现所查找结点!\n"); 

按值查找结果图(头插法建表):
在这里插入图片描述

4、插入运算
(1)后插操作
在查找到的目标结点之后插入新结点。具体操作位在按值查找到插入结点位置以后,将该位置结点的指针指向新插入结点,而新插入的结点的指针指向后一个结点,如图所示:
在这里插入图片描述
代码演示:

s = (LinkList *)malloc(sizeof(LinkList));
printf("\n\n\t\t请输入需要插入的值:");
scanf("%s",&s->data);
printf("\n\t\t需要在原表那个数据后插入:");
scanf("%s",&key);
while (p != NULL)
{
	if(p->data == key)
	{
		s->next = p->next;
		p->next = s;
		Through(head);
		printf("\n\n\t\t已插入!\n");
		return;
	}
	p = p->next;
}	
printf("\n\t\t未发现输入的原表数据\n");

后插结果图(头插法建表):
在这里插入图片描述
(2)前插操作
在查找到的目标结点之前插入新结点,与后插不同,前插需要考虑头结点的影响,由于头结点之前没有新结点,所以需要单独处理(注意:此处可根据尾插法建表处使用的预设空的头结点的思想来改进),即只需要将新结点链接到头结点上,并将其作为头结点即可。而其他位置的前插操作需要新的指针f来辅助完成,即f指向永远跟随在指针p之后,当需要前插数据时,p指针用于按值找到结点位置,而指针f则用于将p找到结点位置的前一个位置的结点的指针指向新生结点,新生结点再指向p指向的结点,即完成新生结点的前插操作,如图所示:
在这里插入图片描述
代码演示:

s = (LinkList *)malloc(sizeof(LinkList));
printf("\n\n\t\t请输入需要插入的值:");
scanf("%s",&s->data);
printf("\n\t\t需要在原表那个数据前插入:");
scanf("%s",&key);
while (p != NULL)
{
	if(p->data == key)
	{
		if(p == head)
		{
			s->next = head;
			head = s;
		}
		else
		{
			f->next = s;
			s->next = p;
		}		
		Through(head);
		printf("\n\n\t\t已插入!\n");
		return;
	}
	f = p;
	p = p->next;
}	
printf("\n\t\t未发现输入的原表数据\n");

前插结果图:
在这里插入图片描述
(3)按位置插入
某些情况下,我们可能需要按位置插入结点。实现该操作也需要遍历链表,使用计数器累加到目标位置,而后在该位置插入结点;同时对于头结点也需要单独处理(注意:此处可根据尾插法建表处使用的预设空的头结点的思想来改进)。
代码演示:

s = (LinkList *)malloc(sizeof(LinkList));
printf("\n\n\t\t请输入需要插入的值:");
scanf("%s",&s->data);
printf("\n\t\t需要在原表那个数据的位置插入:");
scanf("%d",&num);
while (p != NULL)
{
	if(i == num)
	{
		if(num == 1)
		{
			s->next = head;
			head = s;		
		}
		else
		{
			s->next = p->next;
			p->next = s;			
		}
		Through(head);
		printf("\n\n\t\t已插入!\n");
		return;
	}
	i++;
	p = p->next;
}	
printf("\n\t\t未发现输入的原表数据\n");

5、删除运算
和前插运算类似,要删除单链表当中的结点,首先需要一个指针p指向将要被删除的结点,接着使用紧跟着p的指针s来修改指针域,即将s的指针域指向被删除结点后一个结点,最后释放掉删除结点p,被删除的结点将归还到储存池当中;同样对于头结点需要单独处理(注意:此处可根据尾插法建表处使用的预设空的头结点的思想来改进),如图所示:
在这里插入图片描述
代码演示:

printf("\n\n\t\t请输入需要删除的结点:");
scanf("%s",&key);	
while (p != NULL)
{			
	if (p->data == key)
	{
		if(p == head)
		{
			p = p->next;
			free(head);
			head = p;							
		}
		else
		{
			s->next = p->next;
			free(p);
		}								
		Through(head);
		printf("\n\n\t\t删除成功!\n");			
		return;
	}	
	s = p;
	p = p->next;
}
printf("\n\t\t未发现需要删除的结点!\n");

删除结果图(头插法建表):
在这里插入图片描述

6、链表的遍历和长度
链表的遍历即每次从头结点开始,按照指针域的指向依次输出结点数据;其长度在需要得到的时候,遍历链表即可得到。
代码演示:

while (r != NULL)
{
	printf("%c-->", r->data);
	r = r->next;
	length++;//用于获得链表长度
}

7、链表的销毁
销毁即释放所有结点变量空间,循环释放即可。
代码演示:

while (head)
{
	p = head->next;
	free(head);
	head = p;
}

完整代码

#include <stdio.h>
#include <stdlib.h>
typedef char datatype;

typedef struct node
{
	datatype data;
	struct node *next;
}LinkList;

LinkList *head, *p ,*s;
int length = 0;
void Through(LinkList *r)
{
	if(r == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	printf("\n\t\t");
	while (r != NULL)
	{
		printf("%c-->", r->data);
		r = r->next;
		length++;//用于获得链表长度
	}	
}

//头插法建表
void Create()
{
	char ch;
	head = NULL;
	s = NULL;
	scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区,该用法见杂项_深入理解缓冲区及scanf高阶用法	
	printf("\n\t\t请输入一串数据,以‘#’结束!\n\n\t\t");
	ch = getchar();
	while (ch != '#')
	{
		p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
		p->data = ch;	
		p->next = head;
		head = p;
		ch = getchar();
	}
	Through(head); 
	printf("\n\n\t\t链表已建好!\n");
}

/*
//尾插法建表方法一
void Create()
{	
	char ch;
	head = NULL;
	s = NULL;
	scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区,该用法见杂项_深入理解缓冲区及scanf高阶用法
	printf("\n\t\t请输入一串数据,以‘#’结束!\n\n\t\t");
	ch = getchar();
	while (ch != '#')
	{
		p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
		p->data = ch;
		if(head == NULL)
		{
			head = p;
		}
		else
		{
			s->next = p;
		}
		s = p;
		ch = getchar();
	}
	if(s != NULL)
	{
		s->next = NULL;
	}
	Through(head); 	
	printf("\n\n\t\t链表已建好!\n"); 
}
*/

/*
//尾插法建表方法二(注意:使用此方法,对应需要修改其他代码)
void Create()
{	
	char ch;
	head = (LinkList *)malloc(sizeof(LinkList));
	s = head;
	scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区,该用法见杂项_深入理解缓冲区及scanf高阶用法
	printf("\n\t\t请输入一串数据,以‘#’结束!\n\n\t\t");
	ch = getchar();
	while (ch != '#')
	{
		p = (LinkList *)malloc(sizeof(LinkList));//动态生成结点
		p->data = ch;
		s->next = p;
		s = p;
		ch = getchar();
	}
	s->next = NULL;
	printf("\n\n\t\t链表已建好!\n");	
	Through(head->next); //由于头结点为空,所以传入第二个结点 
}
*/

//序号查找
void Number_Find()
{
	int i; 
	int j = 1;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	printf("\n\t\t请输入查找位置:");
	scanf("%d", &i);	
	while (p != NULL)
	{
		if (i == j)
		{
			printf("\n\t\t发现所查找位置结点数据为:%c\n", p->data);
			return;
		}	
		p = p->next;
		j++;		
	}
	printf("\n\t\t未发现所查找位置!\n");	
}

//按值查找
void Value_Find()
{
	int j = 1;
	char key;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	printf("\n\t\t请输入查找的结点值:");
	scanf("%s",&key);
	while (p != NULL)
	{
		if(p->data == key)
		{
			printf("\n\t\t发现所查找结点!\t小标为:%d\n", j);
			return;
		}
		p = p->next;
		j++;
	}
	printf("\n\t\t未发现所查找结点!\n"); 
}

//插入运算(后插)
void Insert()
{
	char key;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	Through(head);
	s = (LinkList *)malloc(sizeof(LinkList));
	printf("\n\n\t\t请输入需要插入的值:");
	scanf("%s",&s->data);
	printf("\n\t\t需要在原表那个数据后插入:");
	scanf("%s",&key);
	while (p != NULL)
	{
		if(p->data == key)
		{
			s->next = p->next;
			p->next = s;
			Through(head);
			printf("\n\n\t\t已插入!\n");
			return;
		}
		p = p->next;
	}	
	printf("\n\t\t未发现输入的原表数据\n");
}

/*
//插入运算(前插)
void Insert()
{
	char key;
	LinkList *f;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	Through(head);
	s = (LinkList *)malloc(sizeof(LinkList));
	printf("\n\n\t\t请输入需要插入的值:");
	scanf("%s",&s->data);
	printf("\n\t\t需要在原表那个数据前插入:");
	scanf("%s",&key);
	while (p != NULL)
	{
		if(p->data == key)
		{
			if(p == head)
			{
				s->next = head;
				head = s;
			}
			else
			{
				f->next = s;
				s->next = p;
			}		
			Through(head);
			printf("\n\n\t\t已插入!\n");
			return;
		}
		f = p;
		p = p->next;
	}	
	printf("\n\t\t未发现输入的原表数据\n");
}
*/

/*
//插入运算(位置)
void Insert()
{
	int num;
	int i=1;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	Through(head);
	s = (LinkList *)malloc(sizeof(LinkList));
	printf("\n\n\t\t请输入需要插入的值:");
	scanf("%s",&s->data);
	printf("\n\t\t需要在原表那个数据的位置插入:");
	scanf("%d",&num);
	while (p != NULL)
	{
		if(i == num)
		{
			if(num == 1)
			{
				s->next = head;
				head = s;		
			}
			else
			{
				s->next = p->next;
				p->next = s;			
			}
			Through(head);
			printf("\n\n\t\t已插入!\n");
			return;
		}
		i++;
		p = p->next;
	}	
	printf("\n\t\t未发现输入的原表数据\n");
}
*/

//删除运算
void Delete()
{
	char key;
	p = head;
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	Through(head);
	printf("\n\n\t\t请输入需要删除的结点:");
	scanf("%s",&key);	
	while (p != NULL)
	{			
		if (p->data == key)
		{
			if(p == head)
			{
				p = p->next;
				free(head);
				head = p;							
			}
			else
			{
				s->next = p->next;
				free(p);
			}								
			Through(head);
			printf("\n\n\t\t删除成功!\n");			
			return;
		}	
		s = p;
		p = p->next;
	}
	printf("\n\t\t未发现需要删除的结点!\n");	
}

//链表长度
void LinkList_Length()
{
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	length = 0;
	Through(head);
	printf("\n\n\t\t链表长度为:%d\n", length);
}

//销毁运算
void Destroy_List()
{
	if (head == NULL)
	{
		printf("\n\t\t空链表!\n");
		return;
	}
	while (head)
	{
		p = head->next;
		free(head);
		head = p;
	}
	printf("\n\t\t已销毁!\n");
}

void main()
{
	int b = 1, k;
	while (b)
	{
		system("cls");
		printf("\n\n\t\t链表上的基本操作\n\n");
		printf("\t\t ----------------------------\n");
		printf("\t\t|1.... 链表建立             |\n");
		printf("\t\t|2.... 序号查找             |\n");
		printf("\t\t|3.... 按值查找             |\n");
		printf("\t\t|4.... 链表插入             |\n");
		printf("\t\t|5.... 链表删除             |\n");
		printf("\t\t|6.... 链表长度             |\n");
		printf("\t\t|7.... 链表销毁             |\n");
		printf("\t\t|0.... 退出                 |\n");
		printf("\t\t ----------------------------\n\n");
		printf("\t\t请选择:");
		scanf("%d", &k);
		switch (k)
		{
		case 1: Create();                  break;	
		case 2: Number_Find();             break;
		case 3: Value_Find();              break;
		case 4: Insert();                  break;
		case 5: Delete();				   break;
		case 6: LinkList_Length();         break;
		case 7: Destroy_List();            break;
		case 0:	b = 0;                      break;
		}
		if (b != 0)  { printf("\n\t\t"); system("pause"); }
	}
}

程序主界面(头插法建表):
在这里插入图片描述

如有错误,欢迎指正!_

参考文献:
数据结构:用C语言描述/唐策善等编著.-北京:高等教育出版社,1995(2011重印)

猜你喜欢

转载自blog.csdn.net/qq_36260974/article/details/84182965