顺序表和链表的实现

本文详细介绍了顺序表和链表的实现,包括单向链表和带头双向链表。顺序表使用动态数组实现,支持尾插、头插、查找、插入和删除操作,但存在空间浪费和增容开销。链表则分为单向和双向,动态申请节点,插入和删除操作更灵活,但需要额外的空间存储指针。带头双向链表在中间插入时效率更高,且便于遍历。文章探讨了不同链表结构在实际应用中的优缺点。
摘要由CSDN通过智能技术生成

顺序表和链表的实现


1.顺序表

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

函数接口实现:

void SeqListInit(SeqList* ps)
{
	assert(ps);
	ps->capacity = 0;
	ps->size = 0;
	ps->a = NULL;
}
void SeqListDestroy(SeqList* ps)
{
	assert(ps);
	if (ps->a)
	{
		free(ps->a);
		ps->capacity = 0;
		ps->size = 0;
	}
}
void checklist(SeqList* ps)
{
	if (ps->capacity <= ps->size)
	{
		int newcount = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->capacity = newcount;
		int *tmp= (int*)realloc(ps->a, sizeof(SLDateType) * newcount);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		ps->a = tmp;
	 }
 }

void SeqListPrint(SeqList* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d->", ps->a[i]);
	}
	printf("NULL\n");
}
//尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
	assert(ps);
	checklist(ps);
	ps->a[ps->size] = x;
	ps->size++;

}
//头插
void SeqListPushFront(SeqList* ps, SLDateType x)
{
	assert(ps);
	checklist(ps);
	int i = ps->size;
	while (i>0)
	{
		ps->a[i] = ps->a[i - 1];
		i--;
	}
	ps->a[0] = x;
	ps->size++;
}
//头删
void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	if (ps->size == 0)
	{
		return;
	}
	int i = 1;
	while (i < ps->size)
	{
		ps->a[i - 1] = ps->a[i];
		i++;
	}
	ps->size--;
}
//尾删
void SeqListPopBack(SeqList* ps)
{
	assert(ps);
	if (ps->size == 0)
	{
		return;
	}
	ps->size--;
}

// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x)
{
	assert(ps);
	checklist(ps);
	for (int i = ps->size; i>pos; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[pos] = x;
	ps->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos)
{
	assert(ps);
	for (int i = pos; i<ps->size-1; i++)
	{
		ps->a[i] = ps->a[i+1];
	}
	ps->size--;
}

在本次实现中,使用了动态版本,使顺序表的内存可以进行更改.

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

2.链表

在此介绍不带头的单向链表,和带头的双向链表

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

1.单向链表

函数接口实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SN* newnode = (SN*)malloc(sizeof(SN));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
// 单链表打印
void SListPrint(SListNode* plist)
{
	SN* cur = plist;
	while (cur!=NULL)
	{
		printf("%d->",cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SN* newnode = BuySListNode(x);
	SN* cur = *pplist;
	if (cur == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		newnode->next = cur->next;
		cur->next = newnode;
	}
	
}
// 单链表的头插
void SListPushFront(SListNode**pplist, SLTDateType x)
{
	assert(pplist);
	SN* newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;

}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(*pplist!=NULL);
	assert(pplist);
	SN* list = *pplist;
	SN* tail = list->next;
	if (tail == NULL)
	{
		free(list);
		*pplist = NULL;
	}
	else
	{
		while (tail->next!=NULL)
		{
			list = tail;
			tail = list->next;
		}

		list->next = tail->next;
		free(tail);
		tail = NULL;
	}

}
// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(*pplist != NULL);
	assert(pplist);
	SN* list = *pplist;
	*pplist = list->next;
	free(list);
	list = NULL;
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SN* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	SN* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	SN* tmp = pos->next;
	pos->next = tmp->next;
	free(tmp);
	tmp = NULL;
}
// 单链表的销毁
void SListDestroy(SListNode**plist)
{
	assert(plist);
	if (*plist == NULL)
	{
		return;
	}
	SN* list = *plist;
	SN* tmp = (*plist)->next;
	while (tmp != NULL)
	{
		free(list);
		list = tmp;
		tmp = list->next;
	}
	free(list);
	list = NULL;
}

在此解释头插和头删使用二级指针的原因:

在此之前,在函数接口内修改当前的变量的值时,只能传递该变量的地址,通过解引用操作来修改该变量的值.

所以此时head指向的链表的第一个节点的地址或者NULL,head是一级指针,修改head的值需要二级指针.

再次提及一下带头的单向链表,及创建一个哨兵位,哨兵位的next指向该链表的第一个节点,使用哨兵位的优点,不需要使用二级指针,剩下的优点,将会在下次关于链表的博客总结出来.

image-20220808193256731

2.带头的双向链表

函数接口实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include"test.h"
ListNode* ListCreate()
{
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL)
	{
		exit(-1);
	}
	head->_next = head;
	head->_prev = head;
	return head;
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	ListNode* tmp;
	while (cur != pHead)
	{
		tmp = cur;
		cur = cur->_next;
		free(tmp);
	}
	free(pHead);
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur!=pHead)
	{
		printf("%d", cur->_data);
		if(cur->_next!=pHead)
		printf("<>");
		cur = cur->_next;
	}
	printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	ListNode* cur = pHead->_prev;
	newnode->_next = cur->_next;
	cur->_next = newnode;
	newnode->_prev = cur;
	pHead->_prev = newnode;
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->_prev;
	if (cur == pHead)
	{
		printf("该双向链表为空,无法删除\n");
		return;
	}
	ListNode* tmp = cur;
	cur = tmp->_prev;
	pHead->_prev = cur;
	cur->_next = tmp->_next;
	free(tmp);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	newnode->_next = pHead->_next;
	pHead->_next = newnode;
	newnode->_prev = pHead;
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	if (cur == pHead)
	{
		printf("该双向链表为空,无法删除\n");
		return;
	}
	pHead->_next = cur->_next;
	free(cur);
	cur = pHead->_next;
	cur->_prev = pHead;
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		if (cur->_data == x)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* cur = pos;
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	newnode->_next = cur;
	newnode->_prev = cur->_prev;
	cur = cur->_prev;
	cur->_next = newnode;
	cur = pos;
	pos->_prev = newnode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* cur = pos;
	ListNode* tmp = pos->_prev;
	tmp->_next = cur->_next;
	free(cur);
	cur = tmp->_next;
	cur->_prev = tmp;
}

image-20220808193310394

由于带头的双向链表可以快速找个前面的节点和后面的节点,中间插入的效率会比单向链表高不少.

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结 构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3.顺序表,链表对比

image-20220808193757535


image-20220808193310394

由于带头的双向链表可以快速找个前面的节点和后面的节点,中间插入的效率会比单向链表高不少.

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结 构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3.顺序表,链表对比

image-20220808193757535


文章照片来自:比特科技

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sleepymonstergod

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值