数据结构----顺序表,链表

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构----顺序表,链表相关的知识,希望对你有一定的参考价值。


Ⅰ 顺序表

1)概念

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

2)静态顺序表

定长数组

typedef struct SeqList
{
	int a[N];//定长数组
	int size;
}SeqList;

3)动态顺序表

动态开辟的数组

typedef struct SeqList
{
	int* a; //动态开辟的数组
	size_t size;
	size_t capacity; // unsigned int
}SeqList;

4)动态顺序表的常见接口

实现接口时需要注意的点

  1. 注意size_t的使用要避免‘-1’的出现,和int比较会出现整形提升
  2. 越界不一定会报错,系统对越界的检查是一种抽查,即对数组设置标志位,越界一般无法检查,越界如果修改到标志位才会检查报错
  3. 访问数据结构建议调用函数,这样最靠谱

定义一个动态顺序表

typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* a;
	size_t size;
	size_t capacity; // unsigned int
}SeqList;

顺序表的初始化
顺序表的销毁
顺序表的扩容

void SeqListInit(SeqList* psl)
{
	assert(psl);
	psl->a = NULL;
	psl->size = psl->capacity = 0;
}
void SeqListDestory(SeqList* psl)
{
	assert(psl);
	if (psl->a != NULL)
	{
		free(psl->a);
		psl->a = NULL;
	}
	psl->capacity = 0;
	psl->size = 0;
}
void SeqListCheckCapcity(SeqList* psl)
{
	assert(psl);
	if (psl->size == psl->capacity)
	{
		int newcapacity = (psl->capacity == 0) ? 4 : 2 * psl->capacity;
		SLDateType* tmp = (SLDateType*)realloc(psl->a, sizeof(SLDateType) * newcapacity);
		if (tmp == NULL)
		{
			perror("SeqListCheckCapcity:");
		}
		else
		{
			psl->a = tmp;
			psl->capacity = newcapacity;
		}
		
}

1.顺序表在pos位置插入x

void SeqListInsert(SeqList* psl, size_t pos, SLDateType x)
{
	assert(psl);
	assert(pos>=0 && pos<=psl->size);
	size_t end = psl->size;
	//size_t end = psl->size - 1;如果这样写会造成-1整形提升
	while (pos < end)
	//while (pos <= end)
	{
		psl->a[end] = psl->a[end - 1];
		end--;
	}
	psl->a[pos] = x;
	psl->size++;
}

2.顺序表删除pos位置的值

void SeqListErase(SeqList* psl, size_t pos)
{
	assert(psl);
	assert(pos>=0 && psl->size > 0 && pos<psl->size);
	size_t end= psl->size;
	while (pos <= end)
	{
		psl->a[pos] = psl->a[pos + 1];
		pos++;
	}
	psl->size--;
}

5)顺序表OJ题

博客链接:顺序表(数组)OJ练习题1


Ⅱ 链表

1)概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的


实际中链表的结构非常多样,以下情况组合起来就有8种链表结构

  1. 单向或者双向
  2. 带头或者不带头
  3. 循环或者非循环

我们最常用的只有两种,一种是单链表,一种是带头双向循环链表

2)单向链表

① 结构:

typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;
② 单向链表的接口实现

头部插入 和 尾部插入 操作较为简单,略

1.单链表的尾删

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	//空
	assert(*pplist);
	//有一个
	if ((*pplist)->next == NULL)
	{
		*pplist = NULL;
	}
	//有两个及以上
	else
	{
		SListNode* cur = *pplist;
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next->next);
		cur->next->next = NULL;
		cur->next = NULL;
	}
}

assert(pplist)作用:防止指针传错
assert(*pplist)作用:防止指针指向的链表为空

2.单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//空
	assert(*pplist);
	//有一个(和两个以上合并)
	/*if ((*pplist)->next == NULL)
	{
		*pplist = NULL;
	}*/
	//有两个及以上
	//else
	//{
	SListNode* cur = *pplist;
	*pplist = cur->next;
	free(cur);
	cur = NULL;
	//}
}

当含有一个或两个元素的时候可以合并写为一种情况

3. 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* tmp = (SListNode*)malloc(sizeof(SListNode));
	if (tmp == NULL)
	{
		perror("申请失败");
	}
	else
	{
		tmp->data = x;
		tmp->next = NULL;
		return tmp;
	}	
}
4. 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}
5.单链表在pos位置之后插入x
// 
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

为什么不在pos位置之前插入?

6.单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
}

为什么不删除pos位置?

7.单链表的销毁
void SListDestory(SListNode** pplist)
{
	SListNode* cur = *pplist;
	SListNode* prev = *pplist;
	while (cur != NULL)
	{
		prev = cur;
		cur = cur->next;
		free(prev);
		prev = NULL;
	}
	*pplist = NULL;
}
③ 链表OJ

博客链接:链表OJ练习1


3)带头双向循环链表

在单向链表中我们要在pos位置之后插入或删除数据很容易,但是要操作pos之前的数据就相对复杂,双向链表就解决了这个问题

① 结构:

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	LTDataType data;
	struct ListNode* next;
}ListNode;
② 带头双向循环链表的接口实现

头插尾插头删尾删均可调用 3,4 接口进行实现,略

1.创建返回链表的头结点.
ListNode* ListCreate()
{
	ListNode* listhead = (ListNode*)malloc(sizeof(ListNode));
	listhead->prev = listhead;
	listhead->next = listhead;
	return listhead;
}
2.双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead;
	while ((cur->next) != pHead)
	{
		if ((cur->data) == x)
			return cur;
		else
			cur = cur->next;
	}
	return NULL;
}
3.双向链表在pos的前面进行插入
// 
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->data = x;
	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;
}
4.双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}
5.双向链表销毁
// 
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = NULL;
		cur = next;
	}
	free(pHead);
	pHead = NULL;
}

Ⅲ 顺序表和链表的比较

顺序表优点

  1. 顺序表支持随机访问,复杂度为O(1),链表不支持随机访问,时间复杂度O(N)
  2. 缓存利用率顺序表高,链表低

链表优点

  1. 顺序表要任意位置插入或修改效率低为O(N),链表只需要修改指针 指向
  2. 顺序表空间不够需要扩容,链表没有容量的概念

区别

  1. 在储存空间上顺序表物理上一定连续,链表在逻辑上连续物理上不一定连续
  2. 顺序表适用于元素高效存储+频繁访问的场景,链表适用于任意位置插入和删除频繁的场景

对于缓存利用率:

关于CPU缓存,详见陈皓大佬与程序员相关的CPU缓存知识

数组物理上是连续的,cpu执行指令运算要访问内存先要取0x10的数据,拿0x10去缓存中找,发现没有(不命中),这个时候会把主存中这个地址开始的一段空间都读进来缓存(因为读4byte和读一段的成本是一样的).下次访问0x14、 0x18…等就会命中


链表物理上不一定连续,当每次不命中读一段都会读后面连续的无关空间,缓存利用率明显低于数组,同时会造成一定的缓存污染

以上是关于数据结构----顺序表,链表的主要内容,如果未能解决你的问题,请参考以下文章

(数据结构)顺序表与链表的基本操作代码以及比较

考研数据结构模板:顺序表链表栈队列

线性表和顺序表的区别

数据结构顺序表——链表

数据结构之线性表代码实现顺序存储,链式存储,静态链表(选自大话数据结构)

数据结构排序算法