[数据结构] 链表接口实现


链表的定义

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

链表与数组

  • 数组:可以方便的遍历查找需要的数据。在查询数组指定位置的操作中,只需要进行1次操作即可,时间复杂度为O(1)
    但这种时间上的便利性,是因为数组在内存中占用了连续的空间,在进行类似的查找或者遍历时,本质是指针在内存中的定向偏移。
    所以当需要对数组成员进行添加删除的操作时,数组内完成这类操作的时间复杂度则变成了O(n)
  • 链表:整体表现上比数组更加高效。例如当进行插入和删除操作时,链表操作的时间复杂度仅为O(1)
    另外,因为链表在内存中不是连续存储的,所以可以充分利用内存中的碎片空间

【本篇博客讲解的是单向、非循环链表的接口实现】


实现

定义表结构

#pragma once	//预处理指令:只编译一次

//定义链表中每一个"结点" 的结构体
typedef struct SListNode {
	SLDataType data;
	struct SListNode *next;
}SListNode;

//定义"链表" 的结构体
typedef struct SList {
	SListNode *first;	//指向链表第一个结点的指针
}SList;

初始化

  • 思路:创建一个链表的头指针,默认为NULL,即初始化操作。
void SListInit(SList *list) {
	assert(list != NULL);
	list->first = NULL;		//成为一个空链表
}

输出

  • 思路:逐个打印每个结点的data值,为了显示统一格式,将尾结点置为NULL
void SListPrint(SList *list) {
	for (SListNode *cur = list->first; cur != NULL; cur = cur->next) {
		printf("%d --> ", cur->data);
	}
	printf("NULL\n");
}

查找

//找到返回 < 结点的地址 > .没找到返回 <NULL>。
//时间复杂度O(n),因为有遍历操作
SListNode* SListFind(SList* list, SLDataType data) {
	for (SListNode *cur = list->first; cur != NULL; cur = cur->next) {		//遍历
		if (cur->data == data) {
			return cur;
		}
	}
	//否则没找到
	return NULL;
}

头删

  • 思路:排除空链表与无链表的情况。将首指针跳过一个元素指向下一个节点,然后释放掉跳过结点的内存空间即可。
//时间复杂度O(1)
void SListPopFront(SList *list) {
	assert(list != NULL);		//表示没有链表
	assert(list->first != NULL);		//表示有链表,但链表为空

	SListNode *old_first = list->first;
	list->first = list->first->next;
	free(old_first);
}

尾删

  • 思路:分类讨论:
  1. 如果链表只有一个结点,执行头插操作就是进行尾插操作。
  2. 但凡链表不是单结点情况,遍历链表直至倒数第二个结点,然后就可以通过下一结点的指针释放尾结点的内存空间。安全起见将释放后的内存置为NULL
//时间复杂度O(n),因为有遍历操作,单链表
void SListPopBack(SList *list) {
	assert(list != NULL);
	assert(list->first != NULL);		// 0 个结点

	if (list->first->next == NULL) {
		//只有 1 个结点,此时尾删就是头删
		SListPopFront(list);
		return;
	}

	//通常情况		>= 2 个结点
	//方案一:
	SListNode *cur;
	for (cur = list->first; cur->next->next != NULL; cur = cur->next);
	// cur 是倒数第二个结点
	free(cur->next);	//cur->next就变成了无效指针了
	cur->next = NULL;

	//方案二:
	//也可以创建一个变量存储变为NULL之前的值
	/*SListNode *last = cur->next;
	cur->next = NULL;
	free(last);*/
}

删除某一结点

  • 思路:如果pos不是尾结点,删除此结点后指向原结点之后的结点。如果是尾结点执行尾删操作即刻。
//时间复杂度O(n),因为有遍历
void SListEraseAfter(SListNode *pos) {
	assert(pos != NULL);

	//方案一
	SListNode *del = pos->next;		//记录旧结点
	pos->next = pos->next->next;
	free(del);

	////方案二
	//SListNode *next = pos->next->next;
	//free(pos->next);
	//pos->next = next;
}

删除首次出现某一数据的结点

  • 思路:遍历链表,查找结点数据,分类讨论:
  1. 如果首元素就是此数值,说明首节点就是希望删除的结点,调用头删函数。
  2. 如果链表中间元素为此数值,将前一个结点的next指向本结点的下一个结点。
void SListRemove(SList *list, SLDataType data) {
	SListNode *previous = NULL;		//赋初值,一般为 NULL
	SListNode *cur = list->first;

	while (cur != NULL && cur->data != data) {
		previous = cur;
		cur = cur->next;
	}
	
	//跳出 while 循环情况:
	//1. cur 为空,表示没有找到
	if (cur == NULL) {
		return;
	}
	
	//2. 找到数据,cur != NULL 且 previous == NULL
	//首结点就是目标结点,进行头删  
	if (previous == NULL) {
		SListPopFront(list);
		return;
	}
	
	//3. 找到数据,cur != NULL 且 previous != NULL
	// cur 是中间结点,也是要删的结点,同时previous是要删除的前一个节点
	previous->next = cur->next;

	//释放掉无效内存
	free(cur);
}

头插

  • 思路:创建一个新结点,然后对结点的内容与next指向进行设置,然后使原链表的头指针指向本结点,本结点的下一结点置为原链表的首结点即可。
//时间复杂度O(1)
void SListPushFront(SList *list, SLDataType data) {
	assert(list != NULL);

	//1. 创建新 Node 空间
	SListNode *node = (SListNode*)malloc(sizeof(SListNode));
	assert(node);
	
	//2. 赋值
	node->data = data;
	
	//3. 新结点第一个结点的下一个结点,也就是原链表的第一个结点
	node->next = list->first;
	list->first = node;
	
	//4. 记录新的第一个结点
	list->first = node;

	//*****前2步也可以用下面封装出的 BuySListNode 函数实现*****
}

尾插

  • 思路:先新建一个结点,设置完成后分类讨论:
  1. 如果要插入的链表为空
    对空链表进行尾插结点,可以直接调用设置好的头插函数进行操作。
  2. 如果要插入的链表非空
    对非空链表进行尾插结点,遍历链表直到最后一个结点,将创建好的结点连接在原链表的尾结点之后。

对于新建结点操作经常使用,所以选择封装成为一个BuySListNode函数直接反复调用。

//时间复杂度O(n),因为有遍历操作,单链表
SListNode *BuySListNode(SLDataType data) {
	SListNode *node = (SListNode*)malloc(sizeof(SListNode));
	assert(node != NULL);
	node->data = data;
	node->next = NULL;

	return node;
}

void SListPushBack(SList *list, SLDataType data) {
	assert(list != NULL);

	//***********************************
	//****   链表中无结点(空链表)的情况  ****
	//***********************************
	if (list->first == NULL) {
		//链表为空的尾插即头插,调用头插函数
		SListPushFront(list, data);
		return;
	}

	//***********************************
	//****     链表中已经有结点的情况    ****
	//***********************************

	//1. 找到最后一个结点
	SListNode *last = list->first;
	for (; last->next != NULL; last = last->next);
	//此时last是最后一个结点

	//2. 申请空间
	SListNode *node = BuySListNode(data);
	last->next = node;
}

在某一结点后插入新结点

  • 思路:新建一个结点,设置完成后将新结点的next指向pos结点之后的结点,然后将pos结点的next指向新结点,完成插入。
//时间复杂度O(n),因为有遍历
void SListInsertAfter(SListNode *pos, SLDataType data) {
	SListNode *node = BuySListNode(data);
	node->next = pos->next;
	pos->next = node;
}

销毁申请内存

  • 思路:从头结点开始遍历链表所有结点,将每个结点所申请的内存空间逐个释放。
//时间复杂度O(n),因为需要遍历
void SListDestroy(SList *list) {
	SListNode *next;	//这里需要一个结点的指针作为过渡,记录下一个结点的指向
	for (SListNode *cur = list->first; cur != NULL; cur = next) {
		next = cur->next;
		free(cur);
	}
	list->first = NULL;
}

最后附上顺序表的接口实现:【https://blog.csdn.net/qq_42351880/article/details/87970232

猜你喜欢

转载自blog.csdn.net/qq_42351880/article/details/87971991