数据结构:线性表那些事儿(图文全面解析数组和链表c语言)

线性表(linear-list)

定义: 是由n(n>=0)个数据元素(结点)a1,a2,a3, ……an组成的有限序列。
其中:n为数据元素的个数,也称为表的长度。当n=0 时,称为空表。非空的线性表(n>0)

线性表的逻辑特征:
(1)有且仅有一个开始结点a1(无直接前趋);
(2)有且仅有一个终端结点an(无直接后继);
(3)其余的结点ai (2≤i≤n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1
(4) ai是属于某个数据对象的元素,它可以是一个数字、一个字母或一个记录。

线性表的特性
(1)线性表中的所有数据元素的数据类型是一致的
(2)数据元素在线性表中的位置只取决于它的序号。
(3)结点间的逻辑关系是线性的。

顺序表:

定义: 用一组连续的存储单元(地址连续)依次存放线性表的各个数据元素。
即:在顺序表中逻辑结构上相邻的数据元素,其物理位置也是相邻的。

顺序表是线性表的一类,其最典型的代表就是数组

顺序表中数据元素的存储地址

若一个数据元素仅占一个存储单元,则其存储方式参见下图。
数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第1张图片
从图中可见,第i个数据元素的地址为:LOC(ai ) = LOC(a1)+(i-1)

小结: 顺序表插入、删除算法平均约需移动一半结点,当n很大时,算法的效率较低。

优点:

  1. 无须为表示结点间的逻辑关系而增加额外的存储空间。
  2. 可以方便地随机存储表中的任一结点。

缺点:

  1. 存储分配只能预先进行(静态)
  2. 插入和删除平均须移动一半结点。
  3. 申请空间过大,浪费内存,过小,溢出。

例:

下面给出一个示例程序,加以辅助说明数组特点。

#include 
int main()
{
	int num[10] = { 0 };
	printf("%d\n", &num[0]);
	printf("%d\n", &num[1]);
	printf("%d\n", &num[2]);
	return 0;
}

运行结果
在这里插入图片描述
此例中声明了一个长度为10的num数组,并初始化所有元素为零(第一个为0,其余默认为0)。其实当你写下int num[10]这条语句后,电脑执行的时候就会自动为你分配10个连续的(不管是逻辑上还是在磁盘中都连续)int类型的内存空间,而num就是第一个int类型的内存的地址。等等!!!你可能会问数组怎么和内存地址扯上关系了?不要慌。

且听我细细道来:电脑中每个内存单元都一个独一无二的地址,当执行完int num[10]语句后,电脑分配的连续空间的首地址给num,其余的地址在num的地址基础上加上元素类型在电脑所占内存(比如一个int类型数据在我电脑里占4个字节,所以num[1]地址在num[0]基础上加4)即可,电脑就根据地址对相应内存进行读取操作。可以说num就是这段内存空间的名字,方便人阅读,而它的值就是内存空间的地址,方便电脑操作。

当你多输出几次会发现每次的地址都不一样,这是因为每次程序开始运行,电脑都是随机给数组分配内存。

结构体数组

思来想去还是把结构体数组放在了顺序表里面,因为它符合顺序表的定义,唯一的区别在于结构体数组一个元素里面可能包含着不同类型的数据类型。
例:

struct Node
{
	int data=0;
	char s;
	······
}node[10];

链表:(linked list)

链表是一种常见而重要的动态存储分布的数据结构,它由若干个统一结构类型的“结点”依次连接组成。它包括两个域:储存数据的数据域和储存后继结点位置的指针域,指针域中储存的信息称做指针或链,指针指向的就是下一个节点的内存地址。n个结点链结成一个链表。

简单来说:用一组任意的存储单元(可以是无序的)存放线性表的数据元素。
无序 —可零散地分布在内存中的任何位置上。

链表的特点
链表中结点的逻辑次序和物理次序不一定相同。即:逻辑上相邻未必在物理上相邻
结点之间的相对位置由链表中的指针域指示,而结点在存储器中的存储位置是随意的。

结点的组成: 数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第2张图片
数据域 — 表示数据元素自身值。
指针域(链域)----表示与其它结点关系;通过链域,可将n个结点按其逻辑顺序链接在一起(不论其物理次序如何)。

链表的优缺点:
优点:

  1. 插入和删除速度快。
  2. 动态分配内存空间,不用事先开辟内存
  3. 提高了内存利用率。

缺点:

  1. 占用额外的空间以存储指针。
  2. 查找速度比较慢。

链表的基本单元:

结点是链表的基本单元,所以在创建链表时要声明结构体。
例:

typedef struct Node {
	char data;
	Node *next;
}node;

单链表:

每个节点中只包含一个指针域的链表称为单链表

头结点—其指针域指向表中第一个结点的指针(头结点不是必须的,只是习惯上加上头结点,而头结点的数据域一般记录的是该链表的相关数据,如:链表长度)。

单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名。

例如:若头指针为head,则可把链表称为“表head”。
数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第3张图片
话不多说,直接来看看链表的相关操作吧!!!

定义结点结构体

typedef struct Node {
	char data;//数据域
	Node *next;//指向下一节点指针
}node;

typedef struct Head {//头结点同理
	int num = 0;
	Node *next;
}Head;

插入

这一部分可以说是链表操作的精华部分,理解这一部分代码很关键哦!

void insert(Head *head,node* nod)//尾插
{
	if (head->next == NULL) {//若链表为空尾结点即为头结点
		head->next = nod;
	}
	else {
		node* tem = head->next;
		while (tem->next != NULL) {//链表不为空则不断往后遍历
			tem = tem->next;
		}
		tem->next = nod;
	}
}

尾插法就是在链表尾部插入新节点,那么要做的第一步当然是找到链表的尾结点了,找到后直接尾结点指针指向新节点就好啦,是不是很简单!!!

void head_insert(Head* head, node* p)//头插
{
	p->next = head->next;
	head->next = p;
}

头插法就是在链表的头部插入新节点,而头结点的指针就是指向链表第一个节点的指针,所以将新节点指针域指向头结点原来指向的节点,再将头结点指向新节点即可。啊这,晕了?没关系,来,上图。
数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第4张图片
如图所示,头插法需要两步完成:1.将新节点指针域指针指向图中首元结点。2.将头结点指针域指针指向新节点(完成这一步的时候图中打×的地方就断开了)。想一想这两步的顺序能否颠倒?答案是不能,至于为什么好好想一想吧!

前插
即为在指定元素前插入新节点

void pre_insert(Head* head, node* p, char element)
{
	if (head->next == NULL) {
		head->next = p;
	}
	else {
		node* pre = head->next, *tem = head->next;//tem前驱节点
		int flag = 2;//控制pre为tem前驱节点的标志
		while (tem != NULL) {
			if (flag != 0) {
				flag--;
			}
			else
				pre = pre->next;
			if (tem->data == element) {
				p->next = tem;
				pre->next = p;
				return;
			}
			tem = tem->next;
		}
	}
	printf("指定元素不存在,插入失败!!!");
}

数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第5张图片

后插
在指定元素后插入新节点。

void l_insert(Head* head, node* p, char element)
{
	if (head->next == NULL) {
		head->next = p;
	}
	else {
		node* tem = head->next;
		while (tem->next != NULL) {//遍历查找指定元素
			if (tem->data == element) {
				break;
			}
			tem = tem->next;
		}
		p->next = tem->next;
		tem->next = p;
		return}
	printf("指定元素不存在,插入失败!!!");
}

数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第6张图片
链表的插入只需要改变指针域指向的节点即可,单链表的后插要比前插简单一点。
删除

void del(Head* head, char element)//删除
{
	node* tem = head->next;//临时节点
	if (head->next == NULL) {//链表为空
		printf("链表为空!!!");
		return;
	}
	else if(head->next->data==element){//第一个节点为目标节点
		head->next = tem->next;
		return;
	}
	else {//第一个节点不是目标节点
		node* pre = head->next;//tem前驱节点
		int flag = 2;//控制pre为tem前驱节点的标志
		while (tem != NULL) {
			if (flag != 0) {
				flag--;
			}
			else
				pre = pre->next;
			if (tem->data == element) {
				pre->next = tem->next;
				free(tem);
				return;
			}
			tem = tem->next;
		}
		printf("链表中没有此元素!!!");
	}
}

数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第7张图片
找到目标节点,使其前驱结点指向其指向的下一个节点即可。

单链表的其它操作比较简单且容易理解,具体看完整代码和注释。

完整代码

#include 
typedef struct Node {
	char data;
	Node *next;
}node;

typedef struct Head {//头结点
	int num = 0;
	Node *next;
}Head;

node* creatnode(char da) //创造节点 
{
	node* newnode = (node*)malloc(sizeof(node));//为p结构体指针分配内存
	newnode->data = da;
	newnode->next = NULL;
	return newnode;
}

void insert(Head *head,node* p)//尾插
{
	if (head->next == NULL) {
		head->next = p;
	}
	else {
		node* tem = head->next;
		while (tem->next != NULL) {
			tem = tem->next;
		}
		tem->next = p;
	}
}

void head_insert(Head* head, node* p)//头插
{
	p->next = head->next;
	head->next = p;
}

void del(Head* head, char element)//删除
{
	node* tem = head->next;//临时节点
	if (head->next == NULL) {//链表为空
		printf("链表为空!!!");
		return;
	}
	else if(head->next->data==element){//第一个节点为目标节点
		head->next = tem->next;
		return;
	}
	else {//第一个节点不是目标节点
		node* pre = head->next;//tem前驱节点
		int flag = 2;//控制pre为tem前驱节点的标志
		while (tem != NULL) {
			if (flag != 0) {
				flag--;
			}
			else
				pre = pre->next;
			if (tem->data == element) {
				pre->next = tem->next;
				free(tem);
				return;
			}
			tem = tem->next;
		}
		printf("链表中没有此元素!!!");
	}
}

void search(Head* head, char element)//查询
{
	int number = 1;
	node* tem = head->next;
	if (tem == NULL) {
		printf("链表为空!!!");
		return;
	}
	while (tem != NULL) {//遍历
		if (tem->data == element) {
			printf("所查找的元素为链表第%d个节点!", number);
			return;
		}
		number++;
		tem = tem->next;
	}
	printf("目标元素不存在!!!");
}

void combine(Head* head1,Head* head2)//合并
{
	node* tem = head1->next;
	if (tem == NULL) {//head1链表为空head1直接指向head2指向的节点
		head1->next = head2->next;
		return;
	}
	while (tem->next != NULL) {//若head1不为空,找head1尾节点,使其指向head2的首节点
		tem = tem->next;
	}
	tem->next = head2->next;
}

Head* resolve(Head* head, char ad)//分解
{
	if (head->next == NULL) {
		printf("链表为空,分解失败!!!");
		return NULL;
	}
	Head* head1 = (Head*)malloc(sizeof(Head));//为新头节点分配内存空间
	head1->next = NULL;
	node* tem = head->next;
	while (tem != NULL) {//寻找目标节点
		if (tem->data == ad) {//将新头节点指向目标节点指向的节点,将目标节点的指针域置为空
			head1->next = tem->next;
			tem->next = NULL;
			return head1;
		}
		tem = tem->next;
	}
	printf("未找到标记点,分解失败!!!");
	return NULL;
}

void sorted(Head* head)//从大到小排序
{
	if (head->next == NULL) {
		printf("链表为空,排序失败!!!");
		return;
	}
	node* tem1 = head->next;
	while (tem1 != NULL) {
		node *tem2 = tem1->next;
		while (tem2 != NULL) {
			if (tem1->data < tem2->data) {
				char p = tem1->data;
				tem1->data = tem2->data;
				tem2->data = p;
			}
			tem2 = tem2->next;
		}
		tem1 = tem1->next;
	}
}

int length(Head* head)
{
	int len = 0;
	node* tem = head->next;
	while (tem != NULL) {
		len++;
		tem = tem->next;
	}
	return len;
}

void print(Head* head)//输出函数
{
	if (head->next == NULL) {
		printf("链表为空,无法输出!!!");
		return;
	}
	else {
		node* tem = head->next;
		while (tem != NULL) {
			printf("%c", tem->data);
			tem = tem->next;
		}
		return;
	}
}

int main()	//主函数只给出初始化和部分测试代码,可自己在主函数添加具体测试调用函数
{
	Head* head = (Head*)malloc(sizeof(Head));
	head->next = NULL;
	Head* head1 = (Head*)malloc(sizeof(Head));
	head1->next = NULL;

	node* p;
	for (int i = 0; i < 5; i++) {//创建head链表
		p = creatnode('A' + i);
		head_insert(head, p);
	}
	
	for (int i = 0; i < 6; i++) {//创建head1链表
		p = creatnode('G' + i);
		head_insert(head1, p);
	}
	return 0;
}

双向链表(Double linked list)

单链表的每个结点再增加一个指向其前趋的指针域 prior,这样形成的链表有两条不同方向的链,称之为双(向)链表。

特点:

  1. 双链表一般也由头指针head唯一确定。
  2. 每一结点均有:
    数据域(data)
    左链域(prior)指向前趋结点.
    右链域(next)指向后继。
    是一种对称结构(既有前趋势,又有后继)
  3. 双链表的前插和后插难度是一样的。

缺点

指针数的增加会导致存储空间需求增加;二是 添加和删除数据时需要改变更多指针的指向。

节点

typedef struct Node{
	Node* pre;
	int data;
	Node* next;
}Node;

双向链表的大部分操作与单链表非常类似,只是在操作的时候改变指针稍稍不同,这里只重点说明一下变化较大的操作。

查询

单链表只能从头结点往后遍历查找,但双向链表可从链表任意位置开始查找。而我给出的示例中是同时从头尾开始向中间遍历查找,这样会加快便利的速度。我这里是在求链表长度的时候记录下尾结点。

int length(Node* head,Node* tail)
{
	int len = 0;
	Node* tem = head->next;
	while (tem->next != NULL) {
		len++;
		tem = tem->next;
	}
	tail->pre = tem;
	return len + 1;
}

void search(Node* head, Node* tail, int element)//查询
{
	int number = 1;
	Node* tem = head->next,* temp = tail->pre;
	if (tem == NULL) {
		printf("链表为空!!!");
		return;
	}
	while (tem != temp && temp->next != tem) {
		if (tem->data == element) {
			printf("所查找的元素为链表第%d个节点!", number);
			return;
		}
		else if (temp->data == element) {
			printf("所查找的元素为链表倒数第%d个节点!", number);
			return;
		}
		number++;
		tem = tem->next;
		temp = temp->pre;
	}
	printf("目标元素不存在!!!");
}

数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第8张图片
如图:tem利用节点next指针从前往后遍历,temp利用节点pre指针从后往前遍历,遍历结束条件为:
若有奇数个节点,则结束时tem一定等于temp;节点为偶数个时,结束时tem->next一定等于temp或者temp->pre一定等于tem。

删除

双链表删除要比单链表简单一些,因为它不需要额外的寻找指定节点的前驱结点。如上图:若要删除p节点,只需将head节点next指针指向rear节点,而rear节点pre指针指向head节点,最后再释放掉p节点所占内存就完成了删除操作。

void del(Node* head, int element)//删除
{
	Node* tem = head->next;//临时节点
	if (head->next == NULL) {//链表为空
		printf("链表为空!!!");
		return;
	}
	while (tem != NULL) {
		if (tem->data == element) {
			tem->pre->next = tem->next;
			tem->next->pre = tem->pre;
			free(tem);
			return;
		}
		tem = tem->next;
	}
	printf("链表中没有此元素!!!");
}

完整代码

#include 
typedef struct Node{
	Node* pre;
	int data;
	Node* next;
}Node;

Node* creat(int number)//产生新节点
{
	Node* newnode = (Node*)malloc(sizeof(Node));
	newnode->data = number;
	newnode->next = newnode->pre = NULL;
	return newnode;
}
void head_insert(Node* head, Node* p)//头插
{
	if (head->next == NULL) {
		p->pre = head;
		head->next = p;
	}
	else {
		p->pre = head;
		p->next = head->next;
		head->next->pre = p;
		head->next = p;
	}
}
int length(Node* head,Node* tail)
{
	int len = 0;
	Node* tem = head->next;
	while (tem->next != NULL) {
		len++;
		tem = tem->next;
	}
	tail->pre = tem;
	return len + 1;
}

void search(Node* head, Node* tail, int element)//查询
{
	int number = 1;
	Node* tem = head->next,* temp = tail->pre;
	if (tem == NULL) {
		printf("链表为空!!!");
		return;
	}
	while (tem != temp && temp->next != tem) {
		if (tem->data == element) {
			printf("所查找的元素为链表第%d个节点!", number);
			return;
		}
		else if (temp->data == element) {
			printf("所查找的元素为链表倒数第%d个节点!", number);
			return;
		}
		number++;
		tem = tem->next;
		temp = temp->pre;
	}
	printf("目标元素不存在!!!");
}
void print(Node* head)//输出函数
{
	if (head->next == NULL) {
		printf("链表为空,无法输出!!!");
		return;
	}
	else {
		Node* tem = head->next;
		while (tem != NULL) {
			printf("%d", tem->data);
			tem = tem->next;
		}
		return;
	}
}
void del(Node* head, int element)//删除
{
	Node* tem = head->next;//临时节点
	if (head->next == NULL) {//链表为空
		printf("链表为空!!!");
		return;
	}
	while (tem != NULL) {
		if (tem->data == element) {
			tem->pre->next = tem->next;
			tem->next->pre = tem->pre;
			free(tem);
			return;
		}
		tem = tem->next;
	}
	printf("链表中没有此元素!!!");
}
void combine(Node* head1, Node* head2, Node* tail)//合并
{
	Node* tem = head1->next;
	if (tem == NULL) {
		head1->next = head2->next;
		head2->next->pre = head1;
		return;
	}
	tail->pre->next = head2->next;
	head2->next->pre = tail->pre;
}
int main()	//主函数只给出初始化和部分测试代码,可自己在主函数添加具体测试调用函数
{
	Node* head = (Node*)malloc(sizeof(Node));//头结点
	head->pre = head->next = NULL;
	head->data = -1;
	
	Node* tail = (Node*)malloc(sizeof(Node));//尾结点
	tail->pre = tail->next = NULL;
	tail->data = -1;
	
	Node* head1 = (Node*)malloc(sizeof(Node));//第二个头结点
	head1->pre = head1->next = NULL;
	head1->data = -1;

	Node* p;
	for (int i = 0; i < 5; i++) {//创建head链表
		p = creat(i);
		head_insert(head, p);
	}
	
	for (int i = 6; i < 11; i++) {//创建head1链表
		p = creat(i);
		head_insert(head1, p);
	}
}

循环链表(Circular linked list)

整个链表形成一个环,从表中任一结点出发均可找到表中其它结点。
数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第9张图片

特点:

  1. 表中最后一个结点的指针指向第一个结点或表头结点(如有表头结点的话)
  2. 循环链表的运算与单链表基本一致。但两者判断是否到表尾的条件不同: 单链表:判断某结点的链域是否为空。循环链表:判断某结点的链域值是否等于头指针。

循环链表的各个操作与单链表和双向链表极为相似,就不在赘述其操作了。这里只说一下两循环链表的合并操作:循环链表一般不标记头节点,而是标记尾节点,这样循环链表的合并就变得非常简单了,只需要是第一个链表的尾节点指向第二个链表的头节点,第二个链表的尾节点指向第一个链表的头节点即可。如图所示:
数据结构:线性表那些事儿(图文全面解析数组和链表c语言)_第10张图片
好了,线性表的内容基本上就这么多了,希望能够就对你有所帮助。欢迎各位小伙伴在下方留言区留言评论或提问!数据结构系列的文章我会持续更新,朋友们下次见!!!

你可能感兴趣的:(数据结构,数据结构,指针,链表)