初级数据结构-->顺序表 单链表讲解〈一〉

                                                        优秀是一种习惯。

文章目录


首先我们先来看看线性表的概念,由以下的概念我看可以得出一个结论:在逻辑结构上为呈线性的我们便称之为线性结构。哪怕物理结构不是呈现线性   ps:结构分为两种 1.逻辑结构 2.物理结构      ps:物理结构包含两种存储方式(顺序存储:静态    链式存储:动态)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16


那么什么是逻辑结构 什么又是物理结构呢?

我们先从 顺序表 来开始了解

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16

 顺序表:本质就是数组,但是顺序表可以动态增长(利用realloc),(频繁扩容的缺点:空间碎片化:空间(硬盘物理空间、内存等)在长时间使用后,造成空间块不连续的现象,叫做空间碎片化。空间使用的时间越长,碎片化就越严重;所以使用内存对齐能让空间碎片没那么多)并且要求里面存储的数据要求按照从左到右的顺序存储。数组要是想增长空间只能用函数realloc来增容。

缺点:1.动态增容有性能消耗可能会造成空间浪费(频繁扩容),原因是顺序表的增容方式为一次增加原数组的2倍或者1.5倍的长度。 2.如果需要头部插入新的数据的话,那么整个数组都要往后移。    

头插中间插需要挪动数据  O(n)

优点:1.可以随机访问数据      2.cpu高速缓存命中率比较高(因为物理空间是连续的,打印数据时系统会预处理呀16个字节)

扩展:要是打印相同长度的顺序表跟链表,那么顺序表打印的速度比链表打印得要快,因为涉及到高速缓存命中率问题。

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16

如图所示:逻辑结构(想象出来的)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_16,color_FFFFFF,t_70,g_se,x_16

而且数组在开辟空间的时候是在栈区(动态顺序表在堆区)上开辟一块连续的空间对数据进行存储的。 

如图所示:物理结构(内存中实际如何存储) 

 从开辟的方式我们也可以看到数组空间是一块连续的空间

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_18,color_FFFFFF,t_70,g_se,x_16    

前面说到顺序表是有缺陷的,所以为了弥补这一个缺陷,又多出了一种方法,名叫:链表。

接下来往下再给大家简单的讲讲链表。

顺序表总结:综合上面两个图可以知道,顺序表在逻辑跟物理结构上是一致的,都为线性。


                        内容温馨提示: 接下来了解下的知识------>单链表

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_17,color_FFFFFF,t_70,g_se,x_16

 

本节我们先来了解下单向链表的一些知识。

节点:用来存储单个数据的东西我们称它为链表的节点,一个节点至少有一个数据域跟指针域(这个指针域用来存储的是下一个节点的地址)指针是用来指向下一个节点的。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16

逻辑结构:单向链表的空间是一块一块独立的空间,链接顺序是前一个的指针指向后一个的数据(可以结合下面的逻辑结构图来理解),所以这样就不要求数据是顺序存储的了,而且在链表后尾增加数据的时候只需要开辟一个独立的空间(malloc开辟来的空间,不能够保证地址是连续的),使之前的尾部指针指向该数据就行,不需要挪动前面的数据。如果数头部加入数据,那就让头部的指针指向原来的头部数据就行,中间加入数据的话就让上一个数据的指针指向新插入的数据,让插入的数据指针指向下一个即可。最后的一个数据的指针指向NULL创建一个指针变量(phead),指针变量存放的是头节点的地址,所以称这个指针变量为头指针或者头节点,存储的是第一个节点的地址。

所以我们认为链表的存储顺序是连续的线性的,用指针把数据都链接了起来。(举例:好比你去买奶茶取了号,取完号以后你可以在奶茶店的任意地方站着,而不需要排成一排等待,虽然不是站着一排,但是奶茶号已经把取餐的人按顺序排列好了,这就相当于上面讲的逻辑结构跟物理结构)

优点:

1.链表的空间开辟是,按需申请空间,好处是不存在扩容的代价,不存在空间浪费。

2.头部或者中间插入数据不需要对数据再进行挪动了

3.任意数据插入删除 O(1)

缺点:不支持随机访问中间的数据,只能按顺序来访问

低命中还会导致缓存污染

如图所示: 逻辑结构(想象出来的)

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_20,color_FFFFFF,t_70,g_se,x_16

 如图所示:物理结构(内存中实际如何存储)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN6Ium55qE6Ium6KGM5YOn,size_16,color_FFFFFF,t_70,g_se,x_16

 可以看到,这些数据都不是连续的,都是由一条链子把他们都链接了起来,所以在物理结构上呈现一个非线性。

链表总结:逻辑结构跟物理结构不一致一个是线性一个是非线性,但是我们也称它为线性表。


接下来将对动态顺序表(因为静态顺序表不实用所以不做展示)跟单链表代码进行一个具体的实现以及讲解 

                                                        首先是动态顺序表

第一步:先创建一个自定义库函数,对头文件 结构体 函数声明进行一个包含,并且通过这里简单了解下动态顺序表的基本功能,分别有:增删查改

                                                        1.自定义头文件

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>


//函数声明  头文件

typedef int SeqDateType;

 typedef struct  SeqList
{
	SeqDateType* a;  //动态指针
	SeqDateType size;  //当前数量大小
	SeqDateType seqcapacity;//总容量大小

}SeqList; //结构体类型重定义 等价于struct  SeqList

//实现增删查改的函数声明
void SeqListInfo(SeqList* p);//初始化
void SeqPushBack(SeqList*p, SeqDateType x);   //尾插
void SeqPushFront(SeqList* p, SeqDateType x);  //头插
void SeqPopBack(SeqList* p, SeqDateType x);  //尾删  
void SeqPopFront(SeqList* p, SeqDateType x); //头删
void SeqListInsert(SeqList* p, SeqDateType x);//中间插入
void SeqListErase(SeqList* p, SeqDateType x);//中间删除
void SeqReCapacity(SeqList* p);//扩容函数
void SeqPrint(SeqList* p);//输出数据
void SeqDel(SeqList* p);//恢复初始化
int SeqFind(SeqList* p,SeqDateType x);//查找数据

第二步:创建两个源文件,一个用来对函数进行实现,一个是主函数。

                                                               2. 主函数代码

#include "seqlist.h"
void Seqtest1()
{
	SeqList s;
	SeqListInfo(&s); //初始化;     数组传参最好用传地址  
	SeqPushBack(&s,1); //尾插
	SeqPushBack(&s,2);
	SeqPushFront(&s, 0);
	SeqPushBack(&s, 3);
	//SeqPrint(&s);
	//SeqListInsert(&s, 2, 30);//在哪个位置插入多少
	//SeqListErase(&s,3);//在哪个位置删除谁
	SeqPopBack(&s,1);  //尾删几位
	//SeqPrint(&s);
	//SeqPopFront(&s, 2); //头删几位
    SeqPrint(&s);
//	SeqPrint(&s);
	SeqDel(&s);
}
int main()
{
	Seqtest1();
	return 0;
}

                                                        3.函数实现的源文件代码

#include "seqlist.h"
//函数的实现
void SeqListInfo(SeqList* p)   //初始化
{
	assert(p);
	p->a = NULL;
	p->size = p->seqcapacity = 0;
}



void SeqDel(SeqList* p) //恢复初始化
{
	free(p->a);
	p->a = NULL;
	p->size = p->seqcapacity = 0;
}



void SeqPrint(SeqList* p)  //打印
{
	assert(p);
	for (int i = 0; i < p->size; i++) 
	{
		printf("%d ", p->a[i]);
	}
	printf("\n");
}



void SeqReCapacity(SeqList* p)  //扩容
{
	assert(p);
		if (p->size == p->seqcapacity)
		{
			SeqDateType newcapacity = p->seqcapacity == 0 ? 4 : p->seqcapacity * 2; //一般开启二倍空间
			SeqDateType* newA = realloc(p->a, sizeof(SeqList) * newcapacity);
			if (newA == NULL)
			{
				printf("Capacity   errno");
				exit(-1);
			}
			p->a = newA; //开辟成功就让原来的a指向新开辟的空间
			p->seqcapacity = newcapacity;
		}
}


void SeqPushFront(SeqList* p, SeqDateType x) //   头插  从后往前挪动
{
	assert(p);
	SeqReCapacity(p);
	int end = p->size - 1;
	while (end >= 0)
	{
		p->a[end + 1] = p->a[end]; //把前一个数据往后挪动,让a[0]的位置空出来后插入新数据就行
		end--;
	}
	p->a[0] = x;
	p->size++;
}


void SeqPushBack(SeqList* p, SeqDateType x) //尾插
{
	assert(p); 
	//插入前判断内存够不够,满没满
	SeqReCapacity(p);
	p->a[p->size] = x;  //因为size是表示目前有几个元素,而最后一个元素的下标为size-1,所以
	p->size++;         //新添加元素的时候把元素覆盖到size的位置就行了
}


void SeqPopBack(SeqList* p,SeqDateType x) //尾删  只需要把最后一个数字删掉就行
{ 
	assert(p);
	assert(p->size > 0); //判读数组中有没有数字
	while (x--)
	{
		--p->size;  //size代表有几个数 所以size--以后 ‘\0'就往前挪动了一个 就相当于把后面的删除了
	}
}

void SeqPopFront(SeqList* p, SeqDateType x) //头删    1 2 3 4 5       2
{
	assert(p);
	assert(p->size > 0);
	int input = 0;
	while (x)
	{
		input = 0;
		while (input < p->size-1)
			{
				p->a[input] = p->a[input + 1];
					input++;
			}
		p->size--;
		--x;
	}
}

//1 2  30 3 4 5 6


//这个函数包括了头插 尾插 中间插的三种功能,只需要把这个接口放到其他两个接口里面就行
void SeqListInsert(SeqList* p, SeqDateType pos, SeqDateType x)//pos:插在哪个位置 x:加入哪个数字
{
	assert(p);
	assert(pos > 0 && pos <= p->size);  //pos == p->size的时候 相当于尾插的接口函数,这时候可以把这个函数副用在尾插函数
	SeqReCapacity(p);
	//利用双下标
	int end = p->size - 1;
	while (end>=pos) //取等于是因为要把pos原本的数字移走,让pos位置空出来
	{
		p->a[end + 1] = p->a[end];
		end--;
	}
	p->a[pos] = x; //把插入的数据覆盖在pos上
	p->size++;
}

//这个接口包含了头删  中间删  尾删的三种功能
void SeqListErase(SeqList* p, SeqDateType pos)
{
	assert(p);
	assert(pos >= 0 && pos < p->size);
	//利用下标,把pos后面的数往前移动把pos覆盖上就行
	int end = p->a + pos; //数组名=首元素地址 首元素地址+i访问第i个位置
	while (end<=p->size-1)
	{
		p->a[end] = p->a[end + 1];
		end++;
	}
	p->size--;
}


                            内容温馨提示:接下来讲解单链表代码的具体实现

                                                        1.自定义头文件

#pragma once
#include <stdio.h>
#include <stdlib.h>

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

void SListPrint(SLTNode* plist);  //打印
void SListPushBack(SLTNode** pplist,SLTDataType x); //尾插
void SListPushFront(SLTNode** pplist, SLTDataType x);//头插
void SListNweNode(SLTDataType x);//创建新空间  头尾插都需要开辟新空间
void SListPopBack(SLTNode** pplist);//尾删
SLTNode* SListFind(SLTNode* plist,SLTDataType x);//查找
void SListInsertAfter(SLTNode* pos, SLTDataType x);//在POS前面增加数据

                                                   2. 主函数代码

#include "SList.h"


void SList1()
{
	SLTNode* plist = NULL; //因为是链表所以用指针来操作 要是不用指针那就是顺序表了
	SListPushBack(&plist, 1);//
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPrint(plist);
	SListPushFront(&plist,0);
	SListPrint(plist);
	SListPopBack(&plist);
	SListPrint(plist);

	SLTNode* pos = SListFind(plist,2);//查找
	if (pos)
	{
		printf("该数据存在\n");
	}
	else
	{
		printf("该数据不存在\n");
	}

	SListInsertAfter(pos, 30);//在POS后面增加数据
	SListPrint(plist);
	
}
int main()
{
	SList1();
	return 0;
}

                                                 3.函数实现的源文件代码

#include "SList.h"

void SListPrint(SLTNode* plist)  //打印链表
{
	SLTNode* cur = plist;
	while (cur != NULL) //判断第一个节点是不是空
	{
		printf("%d->",cur->data);//访问第一个节点的指针域
		cur = cur->next;
	}

	printf("NULL\n");
}

SLTNode* BuySLTNode(SLTDataType x) //返回个结构体类型的节点 所以返回类型为结构体类型
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	node->data = x;
	node->next = NULL;
	//指针域跟数据域都赋值好  头插就让node->next 指向头节点*pplist就行
	//尾插就让尾部最后一个指针域指向node这个节点就行
	return node;
}


void SListPushBack(SLTNode** pplist, SLTDataType x)  //尾插
{
	SLTNode* newnode = BuySLTNode(x); //无论头插尾插都要创建节点 所以先创建好
	//判断刚开始的有没有节点
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	//找尾指针
	else
	{
		SLTNode* tail = *pplist;
		while (tail->next != NULL)   //访问下一个节点的指针域
		{
			tail = tail->next;  //指向下一个节点
		}
		tail->next = newnode; //尾指针域存放尾插节点的地址
	}
}


void SListPushFront(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x); //无论头插尾插都要创建节点 所以先创建好
	newnode->next = *pplist; //直接让创建的那个新节点的next指向头节点就行,让后再让头节点的指针转到第一位newnode
	*pplist = newnode;
}

void SListPopBack(SLTNode** pplist)//尾删
{
	//没有节点
	if (*pplist == NULL)
	{
		return;
	}
	//一个节点
	else if((* pplist)->next == NULL)//如果只有一个节点那么直接释放就行
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SLTNode* prev = NULL; //找到尾部前一个节点
		SLTNode* tail = *pplist;//找到尾部的节点
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail); //释放最后一个节点的空间
		tail = NULL; //把最后一个节点的指针置为空
		prev->next = NULL;//让前一个节点的指针域指向NULL  因为这时候前一个节点已经是最后一个节点了
	}
}

SLTNode* SListFind(SLTNode* plist, SLTDataType x)  //查找X的位置
{
	
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;  //返回节点
		}
		cur = cur->next;
	}

	return NULL; //没找到或者说*pplist为NULL
}


void SListInsertAfter(SLTNode* pos, SLTDataType x)//在POS后面增加数据
{
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* cur = pos->next;
	//newnode->next = pos->next;
	//pos->next = newnode;

	pos->next = newnode;
	newnode->next = cur;
}

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值