数据结构---线性表

线性表

线性表的定义

  • 线性表是n个具有相同属性的数据元素的有限序列。
  • 线性表在逻辑上是线性结构,也就是说连续的一条直线。但是在物理结构上不一定是连续的
  • 线性表在物理结构(存储结构)上一般采用顺序和链式的形式存储

线性表分类

顺序表

顺序表是用一段物理地址连续的存储单元存储元素的线性结构,一般采用数组进行存储。在数组上完成数据元素的增删查改

顺序表一般分为:

  • 静态顺序表:使用定长的数组存储
  • 动态顺序表:使用动态开辟的数组存储

静态顺序表只适合确定需要存储多少数据的场景,如果存储数据量不确定的话,空间开太大浪费,开太小不够用。一般都会去使用动态顺序表,根据情况分配多大的空间。下面将介绍动态顺序表

顺次表的存储结构

图示

cpp 复制代码
typedef int ElemType;
typedef struct SeqList
{
	ElemType* a;
	int size;
	int capacity;
}SeqList;

定义一个动态顺序表需要三个属性

1.存储空间的地址,需要一段空间来维护顺序表,需要知道顺序表的起始地址

2.顺序表的元素个数,记录顺序表的元素个数,

3.顺序表的空间容量,用来分配空间

实现顺序表的主要接口函数

cpp 复制代码
//顺序表初始化
void SeqListInit(SeqList* ps);
//顺序表尾插
void SeqListPushBack(SeqList* ps, ElemType x);
//检查容量
void CheckCapicity(SeqList* ps);
//顺序表尾删
void SeqListPopBack(SeqList* ps);
//顺序表头插
void SeqListPushFront(SeqList* ps, ElemType x);
//顺序表头删
void SeqListPopFront(SeqList* ps);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, ElemType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);
//打印顺序表
void SeqListprintf(SeqList* ps);
//销毁顺序表
void DestroyedSeqList(SeqList* ps);

初始化顺序表

这里先为顺序表申请了2个元素类型的空间大小。

cpp 复制代码
void SeqListInit(SeqList* ps)
{
	ps->a = (ElemType*)malloc(sizeof(ElemType)*2);
	ps->size = 0;
	ps->capacity = 2;
}

顺序表尾插

在尾部插入的时候要考虑两种情况,分别是

  • 顺序表未满尾插:直接将元素放入尾部即可
  • 顺序表已满的情况下,则需要申请更大的空间来存放数据

代码实现:

cpp 复制代码
void SeqListPushBack(SeqList* ps, ElemType x)
{
	assert(ps);
	//检查容量
	CheckCapicity(ps);
	//尾插
	ps->a[ps->size] = x;
	ps->size++;
}

这里将检查容量封装成一个函数,方便后面插入检查继续复用

cpp 复制代码
void CheckCapicity(SeqList* ps)
{
	if (ps->size == ps->capacity)
	{
		int newcapacity = ps->capacity * 2;
		ElemType* tmp = (ElemType*)realloc(ps->a,sizeof(ElemType)*newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
}

顺序表尾删

在尾删时,也应该考虑两种情况,分别是

-顺序表已空时,无需删除

-顺序表未空,直接删除尾部元素即可

代码实现: 这里提供两种写法,一种是暴力检查,程序直接崩溃,一种是防止越界程序可以正常运行

  • 暴力检查版
cpp 复制代码
void SeqListPopBack(SeqList* ps)
{
	//判空
	assert(ps->size > 0);//如果尾删空顺序表,程序直接崩溃
	//删除
	--(ps->size);
}
  • 防止越界版
cpp 复制代码
void SeqListPopBack(SeqList* ps)
{
	//判空
	if (ps->size == 0)
	{
		return;
	}
	//删除
	--(ps->size);
}

顺序表头插

和尾插一样,要考虑是否有空间,但是与尾插不同的地方在于,需要挪动数据进行插入
图解

代码实现

cpp 复制代码
void SeqListPushFront(SeqList* ps, ElemType x)
{
	//检查容量
	CheckCapicity(ps);
	//挪动数据
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	//插入
	ps->a[0] = x;
	++ps->size;
}

顺序表头删

头删和尾删一样,先判空。与尾删不一样的地方在于删完后需要挪动数据
图解

代码实现

cpp 复制代码
void SeqListPopFront(SeqList* ps)
{
	//判空
	if (ps->size == 0)
	{
		return;
	}
	//挪动数据覆盖删除
	int start = 0;
	while (start <= ps->size) 
	{
		ps->a[start] = ps->a[start + 1];
		start++;
	}
	--ps->size;
}

在指定位置插入数据

和头插的思想基本一样
图解

代码实现

cpp 复制代码
void SeqListInsert(SeqList* ps, int pos, ElemType x)
{
	//检查容量
	CheckCapicity(ps);
	//挪动数据
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	//插入
	ps->a[pos] = x;
	++ps->size;
}

在指定的位置删除数据

思想与头删基本一样 图解

代码实现

cpp 复制代码
void SeqListErase(SeqList* ps, int pos)
{
	//判空
	if (ps->size == 0)
	{
		return;
	}
	//挪动数据覆盖删除
	while (pos <= ps->size)
	{
		ps->a[pos] = ps->a[pos + 1];
		pos++;
	}
	--ps->size;
}

有了在指定位置插入和删除前提下,头插,头删,尾插,尾删新写法

头插,头删,尾插,尾删新写法

cpp 复制代码
//头插
void SeqListPushFront(SeqList* ps, ElemType x)
{
	SeqListInsert(ps, 0, x);
}
//头删
void SeqListPopFront(SeqList* ps)
{
	SeqListErase(ps, 0);
}
//尾插
void SeqListPushBack(SeqList* ps, ElemType x)
{
	SeqListInsert(ps, ps->size,x);
}
//尾删
void SeqListPopBack(SeqList* ps)
{
	SeqListErase(ps, ps->size);
}

打印顺序表

cpp 复制代码
void SeqListprintf(SeqList* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

销毁顺序表

动态开辟的内存需要我们主动去释放空间 这里需要主动free

cpp 复制代码
void DestroyedSeqList(SeqList* ps)
{
	free(ps->a);
	ps->a == NULL;
	ps->capacity = ps->size = 0;
}

链表

链表是一种物理结构(储存结构)上不一定连续,不一定是顺序的存储结构,数据元素是通过链表中的指针链接次序实现的

链表的分类

链表有很多种类 两两匹配就一共有八种 这里主要介绍一下单链表(单向不带头不循环)

单链表

单链表的存储结构

图示

链表中的结点一般都是在堆上申请的,从堆上申请的空间,按照一定的规则申请的,两次申请的空间也能相同也可能不相同。用一个指针就能找到下一个结点的空间地址了,从而形成线性关系

cpp 复制代码
typedef int ElemType;
typedef struct SListNode
{
	ElemType data;
	struct SListNode* next;
}SLTNode;

typedef SLTNode* LinkList;//定义链表

定义一个数据域和指针域。数据域用来存放数据,指针域的指针指向下一个结点的空间地址

单链表主要实现的接口函数

cpp 复制代码
//创建新结点
SLTNode* NewSLTNode(ElemType x);
//尾插
void SLTPushBack(SLTNode** phead, ElemType x);
//头插
void SLTPushFront(SLTNode** phead, ElemType x);
//尾删
void SLTPopBack(SLTNode** phead);
//头删
void SLTPopFront(SLTNode** phead);
//单链表查找
SLTNode* SLTNodeFind(SLTNode* phead, ElemType x);
//在pos之前插入
void SLTInsert(SLTNode** phead, SLTNode* pos, ElemType x);
//在pos之后插入
void SLTInsertAfter(SLTNode* pos, ElemType x);
//删除pos位置
void SLTErase(SLTNode** phead, SLTNode* pos);
//删除pos位置后得
void SLTEraseAfter(SLTNode* pos);
//打印
void SLTNodePrintf(SLTNode* ps);

单链表尾插

单链表插入主要分为两种情况

  • 没有结点,单链表是空的情况
  • 有一个以上的结点

注意 :

这里需要一个头指针(pehad 指向第一个结点的指针)来维护这个链表。否则将无法寻找到这个链表

cpp 复制代码
void SLTPushBack(SLTNode** phead, ElemType x)
{
	//申请结点
	SLTNode* newnode = NewSLTNode(x);
	//空链表
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	//有一个以上的结点
	else
	{
		SLTNode* tail = *phead;
		//遍历找最后一个结点
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//连接新结点
		tail->next = newnode;
	}
}

链表结点的类型是struct SListNode* (结构体指针)类型,插入一个新元素,需要改变头指针的指向,所以实参需要传其地址,形参需要一个结构体指针的指针才可接受这个地址即二级指针
每次进行插入操作时都要申请结点,封装成函数,方便复用

动态申请新节点

cpp 复制代码
SLTNode* NewSLTNode(ElemType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if(newnode == NULL)
	{
		perror("malloc fail");
		eixt(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

单链表头插

头插可以只看作一种情况 空和非空的处理结果都一样

图解 代码实现

cpp 复制代码
void SLTPushFront(SLTNode** phead, ElemType x)
{
	//申请结点
	SLTNode* newnode = NewSLTNode(x);
	//空和非空链表都可处理
	newnode->next = *phead;
	*phead = newnode;
}

单链表的尾删

尾删要注意三种情况,分别是

  • 空链表
    错误处理
  • 只有一个结点
    直接释放该结点即可
  • 有两个结点以上的链表
    先找到最后一个结点,记录最后一个结点的前一个 然后释放最后一个结点,再将最后一个的前一个指针域置为NULL

代码实现

cpp 复制代码
void SLTPopBack(SLTNode** phead)
{
	//空链表
	assert(*phead);
	//只有一个结点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	//有两个结点以上的链表
	else
	{
		SLTNode* tail = *phead;
		SLTNode* tailprev = NULL;//记录最后一个的前一个
		while (tail->next != NULL)
		{
			tailprev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		tailprev->next = NULL;
	}
}

单链表的头删

头删时要注意两种情况分别是

  • 空链表
    错误处理
  • 有一个或多个结点
    先将头指针移动到第二个结点(只有一个结点第二个结点即为NULL也符合逻辑)的位置,在释放该结点

代码实现

cpp 复制代码
void SLTPopFront(SLTNode** phead)
{
	//空
	assert(*phead);
	//一个和多个结点处理逻辑一样
	SLTNode* newhead = (*phead)->next;
	free(*phead);
	*phead = newhead;
}

在指定位置之前插入

位置由自己指定

比如链表元素 1 2 3 4 在2的位置之前插入6 链表变为1 6 2 3 4 插入之前首先要找到该元素结点的位置

单链表查找

cpp 复制代码
SLTNode* SLTNodeFind(SLTNode* phead, ElemType x)
{
	assert(phead);
	SLTNode* pos = phead;
	while (pos)
	{
		if (pos->data == x)
		{
			return pos;
		}
		pos = pos->next;
	}
	//没有该元素
	return NULL;
}

然后根据查找到元素的结点位置进行插入

插入

在指定位置插入时要考虑以下情况

  • 空链表
    不需要做处理 因为空链表找不到指定的位置
  • 指定位置不存在
    错误处理
  • 指定的位置是第一个结点
    复用头插即可
  • 其他情况下插入
    申请新节点 找到pos的前一个结点 将新结点连接接起来
cpp 复制代码
void SLTInsert(SLTNode** phead, SLTNode* pos, ElemType x)
{
	assert(*phead);
	assert(pos);
	
	if (pos == *phead)
	{
		SLTPushFront(phead, x);
	}
	else
	{
		//申请结点
		SLTNode* newnode = NewSLTNode(x);
		//找pos的前一个
		SLTNode* cur = *phead;
		SLTNode* posprev = NULL;
		while (cur != pos)
		{
			posprev = cur;
			cur = cur->next;
		}
		posprev->next = newnode;
		newnode->next = pos;
	}
}

在指定位置之后插

比如链表元素 1 2 3 4 在2的位置之后插入6 链表变为1 2 6 3 4 和指定位置之前插入一样,首先要找到该元素结点的位置在进行插入 在指定位置后插入要考虑以下情况

  • 空链表
    不需要做处理 因为空链表找不到指定的位置
  • 指定位置不存在
    错误处理
  • 其他情况下插入

这里不用考虑插入的位置是最后一个结点的位置,这样首先要遍历链表进行判断,在复用尾插,代价太大。

代码实现

cpp 复制代码
void SLTInsertAfter( SLTNode* pos, ElemType x)
{
	assert(pos);
	//申请新结点
	SLTNode* newnode = NewSLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除指定位置元素

删除指定位置和插入指定位置一样,需要先查找到该元素结点的位置 比如链表元素 1 2 3 4 删除2的位置链表变为 1 3 4 删除pos位置要考虑以下情况

  • 空链表
    不需要做处理 因为空链表找不到指定的位置
  • 指定位置不存在
    错误处理
  • 指定位置是第一个结点 复用头删
  • 其他情况下删除指定位置

代码实现

cpp 复制代码
void SLTErase(SLTNode** phead, SLTNode* pos)
{
       //空链表
	assert(*phead);
	// 指定位置不存在
	assert(pos); 
	//复用头删
	if (pos == *phead)
	{
		SLTPopFront(phead);
	}
	else
	{
		//找pos前一个
		SLTNode* cur = *phead;
		SLTNode* prevpos = NULL;
		while (cur != pos)
		{
			prevpos = cur;
			cur = cur->next;
		}
		prevpos->next = pos->next;
		free(pos);
	}
}

删除指定位置之后的元素

比如链表元素 1 2 3 4 删除2之后位置 链表变为 1 2 4 删除指定位置之后的元素分别要考虑以下情况

  • 空链表
    不需要做处理 因为空链表找不到指定的位置
  • 指定位置不存在
    错误处理
  • 是否是尾结点
    错误处理
  • 其他情况下删除指定位置之后

代码实现

cpp 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* posnesxt = pos->next;
	pos->next = posnesxt->next;
	free(posnesxt);
	posnesxt = NULL;
}

顺序输出链表

cpp 复制代码
void SLTNodePrintf(SLTNode* phead)
{
	SLTNode* tail = phead;
	while (tail != NULL)
	{
		printf("%d " , tail->data);
		tail = tail->next;
	}
	printf("\n");
}

销毁单链表

cpp 复制代码
void SLTNodeDestory(SLTNode** phead)
{
	assert(*phead);
	SLTNode* cur = *phead;
	SLTNode* curnext = NULL;
	while (cur != NULL)
	{
		curnext = cur->next;
		free(cur);
		cur = curnext;
	}
}

顺序表和单链表的区别

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,物理上不一定连续
随机访问 O(1) O(n)
任意位置插入或删除 可能需要挪动数据,效率太低O(n) 只需要修改指针指向即可
插入元素 动态顺序表,空间不够时需要扩容 没有容量概念,用多少申请多少
应用场景 元素高效存储+频繁访问 频繁在任意位置插入和删除

关于指针传参

当你传递一个参数给函数的时候,这个参数会不会在函数内被改动决定了函数参数形式

  • 如果需要改动,则需要传指向这个参数的指针
    比如单链表的头插,尾插、头删等,都需要改变头指针的指向位置,也就是这个参数需要被改动,那么传这个参数的指针
  • 如果不用被改动,可以直接传递这个参数
    比如单链表中的查找和打印,直接传参数就可以了,查找和打印,不用修改里面的内容
相关推荐
wclass-zhengge2 小时前
数据结构与算法篇(树 - 常见术语)
数据结构·算法
夜雨翦春韭2 小时前
【代码随想录Day31】贪心算法Part05
java·数据结构·算法·leetcode·贪心算法
C++忠实粉丝8 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
ZZZ_O^O8 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
代码雕刻家9 小时前
数据结构-3.9.栈在递归中的应用
c语言·数据结构·算法
Kalika0-011 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
代码雕刻家11 小时前
课设实验-数据结构-单链表-文教文化用品品牌
c语言·开发语言·数据结构
小字节,大梦想12 小时前
【C++】二叉搜索树
数据结构·c++
我是哈哈hh13 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
丶Darling.13 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树