【数据结构和算法】 - 双向链表

学习目标:

  • 双向链表的设计和实现

学习内容:

一、双向链表的头文件

#define _CRT_SECURE_NO_WARNINGS 1
//带头的双向循环链表
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int LTDataType;

typedef struct ListNode
{
    
    
	struct ListNode* next;//存下一个结点的地址
	struct ListNode* prev;//存上一个结点的地址
	LTDataType data;
}ListNode;

//初始化带头结点
//void ListInit(ListNode** pphead);
ListNode* ListInit();

//打印
void ListPrint(ListNode* phead);

//创建结点
ListNode* BuyListNode(LTDataType x);

//尾插 - 单链表尾插可以不找尾,定义一个尾指针
void ListPushBack(ListNode* phead, LTDataType x);

//尾删
void ListPopBack(ListNode* phead);

//头插
void ListPushFront(ListNode* phead, LTDataType x);

//头删
void ListPopFront(ListNode* phead);

//任意位置插入
void ListInsert(ListNode* pos, LTDataType x);

//任意位置删除
void ListErase(ListNode* pos);

//查找
ListNode* ListFind(ListNode* phead, LTDataType x);//返回找到的数的下标
//...
//int SeqListSort(SL* psl, SLDataType x);
//int SeqListBinaryFind(SL* psl, SLDataType x);

//空间销毁
//void ListDestory(ListNode* phead);
void ListDestory(ListNode** phead);

//数据清理 - 清理所有的数据节点,保留head头结点,可以继续使用
void ListClear(ListNode* phead);

#include <stdbool.h>
//判断链表是否为空 - 判断phead->next是否等于phead
bool ListEmpty(ListNode* phead);

//计算链表的长度
int ListSize(ListNode* phead);

注意:
双向链表的实现结构是带哨兵位的头节点的双向循环链表。


二、代码实现
1、ListNode* ListInit();初始化带头结点
void ListInit(ListNode** pphead);初始化带头结点

//传值调用:不会改变外面的值
void ListInit(ListNode* phead)
{
    
    
	phead = BuyListNode(0);
	phead->next = phead;
	phead->prev = phead;
}

优化方法1:

//传址调用
void ListInit(ListNode** pphead)
{
    
    
	//通过外面传的地址在函数里面,改变函数外面的变量的值
	*pphead = (ListNode*)malloc(sizeof(ListNode));//创建一个哨兵位头节点
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;
}

优化方法2:

//方法二:
//返回哨兵位结点的地址
ListNode* ListInit()
{
    
    
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));//创建一个哨兵位头节点
	if (phead == NULL)
		return NULL;
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

2、ListNode* BuyListNode(LTDataType x);创建结点

//创建结点
ListNode* BuyListNode(LTDataType x)
{
    
    
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if(node == NULL)
        return NULL;
	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

3、void ListPrint(ListNode* phead);打印
从第一个结点(非哨兵位)开始,找到哨兵位结点停止打印。

void ListPrint(ListNode* phead)
{
    
    
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
    
    
		printf("%d -> ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

4、void ListPushBack(ListNode* phead, LTDataType x);尾插
单链表尾插可以不找尾,定义一个尾指针。

void ListPushBack(ListNode* phead, LTDataType x)
{
    
    
	assert(phead);//链表为空,即哨兵结点开辟空间失败。一般不会失败,即一定哨兵位结点地址不为空,也不需要断言
	//找尾
	ListNode* tail = phead->prev;
	//插入新结点
	ListNode* newnode = BuyListNode(x);
	//链接:phead、tail、newnode
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
	//注意:(只有一个哨兵位结点)没有结点是否满足
	//注意:也不需要像不带头单链表一样,考虑没有结点的情况
}

解读:
因为不需要改变传过来的哨兵位指针,故不需要传二级指针。
改变的是哨兵位结构体里面的内容,可以直接使用哨兵位指针修改。


5、void ListPushFront(ListNode* phead, LTDataType x);头插
在head结点后插入,保存哨兵位下一个结点的地址。

void ListPushFront(ListNode* phead, LTDataType x)
{
    
    
	assert(phead);//assert(表达式);表达式为真,则什么都不干。表达式为假,则报错。
	//方法一
	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;

	//方法二:
	/*
	//顺序不能颠倒
	ListNode* newnode = BuyListNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
	*/
	//注意:(只有一个哨兵位结点)没有结点是否满足
}

6、void ListPopFront(ListNode* phead);头删
在head结点后删除,保存第一个结点的下一个结点的地址。

void ListPopFront(ListNode* phead)
{
    
    
	assert(phead);
	assert(phead->next != phead);//判断链表为空时,继续删除的断言
	//方法一:
	/*
	ListNode* first = phead->next;
	phead->next = first->next;
	first->next->prev = phead;
	//注意释放位置
	free(first);
	*/

	//方法二:
	ListNode* first = phead->next;
	ListNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;//无需此行
	//注意:(只有一个哨兵位结点)没有结点是否满足
}

7、void ListPopBack(ListNode* phead);尾删
保存要删结点的前一个结点的地址。

void ListPopBack(ListNode* phead)
{
    
    
	assert(phead);
	assert(phead->next != phead);//判断链表只有哨兵位结点(为空)时,继续删除的断言
	//方法一:
	ListNode* tail = phead->prev;
	ListNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;//可以不置空,因为tail为局部变量,出了局部范围自动释放

	//方法二:
	/*
	ListNode* tail = phead->prev;
	phead->prev = tail->prev;
	tail->prev->next = phead;
	//注意释放位置
	free(tail);
	*/
	//注意:(只有一个哨兵位结点)没有结点是否满足
}

8、void ListInsert(ListNode* pos, LTDataType x);任意位置插入
在pos节点的前面插入x,保存pos的前一个结点的地址。

void ListInsert(ListNode* pos, LTDataType x)//注意:不需要传链表
{
    
    
	assert(pos);
	//如果需要在空链表插入,需要结合phead指针。
	ListNode* posPrev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	//链接:posPrev、newnode、pos
	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;

	//方法二:
	/*
	//注意链接顺序
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	newnode->next = pos;
	pos->prev = newnode;
	*/
	//注意:pos在哨兵位结点,相当于尾插
	//注意:pos在第一个结点,相当于头插

	//注意:(只有一个哨兵位结点)没有结点是否满足
	//因为phead->prev==phead;phead->next==phead;对于头插(phead->next),尾插(phead)的结点都不为空,可以满足要求assert(pos);
}

解读:
1、pos在哨兵位结点,相当于尾插
2、pos在第一个结点,相当于头插

//优化:尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
    
    
	assert(phead);
	//尾插:在pos为phead的前面插入x
	ListInsert(phead, x);
}
//优化:头插 - 在head结点后插入
void ListPushFront(ListNode* phead, LTDataType x)
{
    
    
	assert(phead);
	//头插:在pos为phead->next的前面插入x
	ListInsert(phead->next, x);
}

9、void ListErase(ListNode* pos);任意位置删除
删除pos位置的值,保存pos前一个和后一个结点的地址。

void ListErase(ListNode* pos)
{
    
    //不能对空链表删除,也就是不能删除哨兵位结点。
	assert(pos);
	//方法一:
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;
	free(pos);
	posPrev->next = posNext;
	posNext->prev = posPrev;

	//方法二:
	/*
	//注意链接顺序
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	*/
	//注意:pos在哨兵位的前一个结点,相当于尾删
    //注意:pos在第一个结点,相当于头删

	//注意:(只有一个哨兵位结点)没有结点是否满足
	//因为phead->prev==phead;phead->next==phead;对于头删(phead->next),尾删(phead->prev)的结点都不为空,可以满足要求assert(pos);
	//但不可以删除。
}

解读:
1、pos在哨兵位的前一个结点,相当于尾删
2、pos在第一个结点,相当于头删

//优化:尾删
void ListPopBack(ListNode* phead)
{
    
    
	assert(phead);
	assert(phead->next != phead);//判断链表只有哨兵位结点(为空)时,继续删除的断言。或者assert(phead->prev != phead);
	ListErase(phead->prev);
}
//优化:头删 - 在head结点后删除
void ListPopFront(ListNode* phead)
{
    
    
	assert(phead);
	assert(phead->next != phead);//判断链表为空时,继续删除的断言
	ListErase(phead->next);
}

10、ListNode* ListFind(ListNode* phead, LTDataType x);查找,返回找到的数的下标
返回找到的数对应结点的地址,也能作为修改功能的基础。

ListNode* ListFind(ListNode* phead, LTDataType x)
{
    
    
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
    
    
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	//走到这里没有找到
	return NULL;
}

11、void ListDestory(ListNode* phead);空间销毁,销毁创建的链表
释放结点后,里面的值为随机值,找不到下一个结点:先保存当前结点的下一个结点,释放当前结点,再迭代。

void ListDestory(ListNode* phead)
{
    
    
	assert(phead);
	//第一种方法:
	ListNode* cur = phead->next;
	while (cur != phead)
	{
    
    
		ListNode* next = cur->next;
		free(cur);
		cur = next;//迭代
	}
	//来到这里,1、如果链表不使用,则删除head,2、如果链表继续使用,则不删除head
	//假设如果链表继续使用,则不删除head
	phead->next = phead;
	phead->prev = phead;

	//假设如果链表不使用,则删除head
	free(phead);
	phead = NULL;//本身phead需要置空,通过调试发现函数里面置空了,但是函数外面没有作用,因为head和phead它们是两个独立的空间
    
    //第二种方法:
    //1、如果链表不使用,则删除head
	ListClear(phead);
	free(phead);//释放空间,让其回归内存,即变量的地址仍不变,但是地址的值为随机值,空间还给了操作系统
	phead = NULL;//本身phead需要置空,通过调试发现函数里面置空了,但是函数外面没有作用,因为head和phead它们是两个独立的空间
    
	//解决方法:
	//1、在外面主函数最末尾补一行phead = NULL;(不建议)
	//2、二级指针:即传址调用,函数里面想要改变函数外面的值。传值调用:函数里面不改变函数外面的值。
	//3、使用返回值,外面用同一个变量接收返回值
}

优化:

//第三种方法:传二级指针
void ListDestory(ListNode** pphead)
{
    
    
    //1、如果链表不使用,则删除head
	assert(*pphead);
	ListClear(*pphead);
	free(*pphead);//释放空间,让其回归内存,即变量的地址仍不变,但是地址的值为随机值,空间还给了操作系统
	//2、二级指针
	*pphead = NULL;
}

12、void ListClear(ListNode* phead);数据清理
清理所有的数据节点,保留head头结点,可以继续使用。例如:购物车业务

void ListClear(ListNode* phead)
{
    
    
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
    
    
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//来到这里,1、如果链表不使用,则删除head,2、如果链表继续使用,则不删除head
	//释放掉所有数据结点,让头结点head保持带头双向循环链表结构
	phead->next = phead;
	phead->prev = phead;
}

三、代码测试

#define _CRT_SECURE_NO_WARNINGS 1
//带头 双向 循环 链表
#include "List.h"
void TestList1()
{
    
    
    //方法一:
	/*ListNode* head = NULL;
	ListInit(&head);*/

	//方法二:
	ListNode* head = NULL;
	head = ListInit();

    //方法三:直接在外面初始化
	/*ListNode* head = NULL;
	head->next = head;
	head->prev = head;*/

	ListPushBack(head, 1);
	ListPushBack(head, 2);//传入的是ListNode*类型,形参接受也用ListNode*
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPrint(head);

	ListPopBack(head);
	ListPopBack(head);
	ListPopBack(head);
	ListPrint(head);

	ListPushFront(head, -1);
	ListPushFront(head, -2);
	ListPushFront(head, -3);
	ListPrint(head);

	ListPopFront(head);
	ListPopFront(head);
	ListPrint(head);

	//销毁空间
	ListDestory(&head);
}
void TestList2()
{
    
    
	ListNode* head = NULL;
	head = ListInit();
	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPrint(head);

    //查找,并修改
	ListNode* pos = ListFind(head, 3);
	if(pos)
    {
    
    
        pos->data *= 10;
    }
    ListPrint(head);

    //在pos的前面插入30
    ListInsert(pos, 30);
	ListPrint(head);

	//删除4
	pos = ListFind(head, 4);
	if (pos)
	{
    
    
		ListErase(pos);
	}
	ListPrint(head);

	//将1改为10
	pos = ListFind(head, 1);
	if (pos)
	{
    
    
		pos->data = 10;
	}
	ListPrint(head);

	//销毁空间
	//ListDestory(head);
	ListDestory(&head);

}
 int main()
{
    
    
	//TestList1();
	TestList2();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_48163964/article/details/130133109