初步认识顺序表和链表

217 阅读5分钟

顺序表

1.线性表

线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表,链表,栈,队列,字符串...

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

顺序表的直观图.png

要求数据是连续存储(位置)

链表的直观图.png

用指针将一块一块的内存链接起来

2.顺序表

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

顺序表就是数组,但是在数组的基础上,它还要求数据是从头开始存并且是连续存储的,不能跳跃间隔

顺序表的缺陷:

1.空间不够了需要增容,增容是要付出代价

扩容有两种方式:

(1)原地扩容:如果数组的空间后面还有,直接在后面扩容,返回的是原来的指针

原地扩容的情况.png

(2)异地扩容:如果数组空间后面没有其他空间,则重新开辟空间将原数组的数据拷贝到新的空间当中并且返回的是新的指针

异地扩容的情况.png

原地扩容和异地扩容.png

2.避免频繁扩容,我们基本都是扩大2倍,可能就会导致一定的空间浪费

3.要求顺序表的数据从开始位置连续存储,那么在头部或者中间位置插入删除数据就需要挪动数据,效率不高

3.链表

针对顺序表缺陷,就设计出了链表

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

链表的一种结构:

链表的一种结构.png

(1)从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

(2)现实中的结点一般是从堆上申请出来的

(3)从堆上申请空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

假设在32位系统上,结点中值域为int类型,则一个结点的大小为8个字节,则也可能有下述链表:

32位系统的链表.png

(1)单链表

错误版本:

void TestSList1()
{
	SLTNode* plist = NULL;
	SListPushBack(plist, 1);
	SListPushBack(plist, 2);
	SListPushBack(plist, 3);
	SListPushBack(plist, 4);


	SListPrint(plist);
}
void SListPushBack(SLTNode* phead, SLDateType x)//尾插
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//找到尾节点
		SLTNode* tail = phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//找到tail之后开始找新结点

		tail->next = newnode;
	}
}

plist和phead的调试.png

问题:形参phead的改变没有影响实参plist

正确版本:

void TestSList1()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);


	SListPrint(plist);
}
void SListPushBack(SLTNode** pphead, SLDateType x)//尾插
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//找到tail之后开始找新结点

		tail->next = newnode;
	}
}

单链表的正确版本(二级指针).png

单链表尾插的结果.png

(2.)双向链表

链表分为单向,双向,带头[哨兵位(不存储有效数据)],不带头,循环和非循环。排列组合起来总共就有8种。其中有2种较为重要

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶,图的邻接表等

链表的结构1.png

2.带头双向循环链表:结构最复杂,一般用在单独存储数据,实际中使用的链表数据结构,都是带头双向循环链表。

链表的结构2.png 双链表的结构体及接口函数

typedef struct SListNode
{
	LTDateType data;
	struct SListNode* prev;
	struct SListNode* next;
}LTNode;
LTNode* ListInit();
void ListPrint(LTNode* phead);
void ListPushBack(LTNode* phead, LTDateType x);//尾插
void ListPopBack(LTNode* phead);//尾删
void ListPushFront(LTNode* phead,LTDateType x);//头插
void ListPopFront(LTNode* phead);//头删
LTNode* ListFind(LTNode* phead, LTDateType x);//查找函数
void ListInsert(LTNode* pos, LTDateType x);//在pos位置之前插入
void ListErase(LTNode* pos);//删除pos位置
void ListDestroy(LTNode* phead);//销毁链表

4.顺序表和链表的优缺点

(1)顺序表

优点:

a.支持随机访问。需要随机访问结构支持算法可以很好的适用。

b.cpu高速缓存命中率更高

缺点:

a.头部中部插入删除时间效率低。O(N) [需要挪动数据]

b.连续的物理空间,空间不够了以后需要增容(增容有一定程度的消耗。为了避免频繁增容,一般我们都按倍数去增,用不完可能存在一定的空间浪费。)

(2)链表

优点(双链表):

a.任意位置插入删除效率高。O(1)

b.按需申请释放空间。

缺点:

a.不支持随机访问。(用下标访问)意味着:一些排序(快排),二分查找等在这种结构上不适用。

b.链表存储一个值,同时要存储链接指针,也有一定的消耗。

c.cpu高速缓存命中率更低

(3)高速缓存

高速缓存区的直观图.png

为什么说顺序表的命中率会更高呢?其实最主要还是因为顺序表连续的物理空间

顺序表的命中率更高的原因.png