基于双链表实现线性表 List 的典型操作

168 阅读5分钟

前言

在数据结构的学习中,双链表是一种非常重要的线性数据结构。它通过每个节点包含两个指针,一个指向其前驱结点,另一个指向其后继结点,从而提供了双向的访问能力。这种特性使得双链表在某些场景下比单链表更加灵活和高效。本文将详细介绍如何基于带头结点的双链表来实现线性表List1,并演示一些基本操作,如初始化、判空、求表长、插入、删除、查找、修改、遍历以及销毁等。总体设计思路图见下:

一、双链表的定义

首先,我们定义了双链表的节点类型DLinkNode,每个节点由三部分组成:存储的数据data,指向前驱结点的指针prior,以及指向后继结点的指针next

typedef int ElemType;
typedef struct DNode {
    ElemType data;
    struct DNode *prior; // 指向前驱结点
    struct DNode *next;  // 指向后继结点
} DLinkNode; // 声明双链表结点类型

二、双链表的基本操作

2.1 初始化(InitList)

初始化线性表时,我们需要创建一个头结点作为整个链表的起点。头结点不用于存储实际数据,而是方便对链表进行操作。该结点的头指针和尾指针均为空。即:

代码为:

void InitList(DLinkNode*& L) {
    L = (DLinkNode*)malloc(sizeof(DLinkNode));   // 创建头结点
    L->prior = L->next = NULL; // 头结点的前后指针均为空
}

2.2 判空(ListEmpty)

判断链表是否为空只需检查头结点的next指针是否为NULL

bool ListEmpty(DLinkNode* L) {
    return(L->next == NULL);
}

2.3 求表长(ListLength)

为了获取链表长度,我们需要从头到尾遍历整个链表,同时记录经过的结点个数。即:

代码为:

int ListLength(DLinkNode* L) {
    DLinkNode* p = L;
    int i = 0;  // 计数器设置为0
    while (p->next != NULL) {  // 找尾结点p
        i++;  // i对应结点p的序号
        p = p->next;
    }
    return(i);  // 返回链表长度
}

2.4 插入(ListInsert)

插入元素需要找到插入位置的前一个结点,然后创建新结点并调整指针。即:

代码为:

bool ListInsert(DLinkNode*& L, int i, ElemType e) {
    int j = 0;
    DLinkNode* p = L, * s;
    if (i <= 0) return false;  // 检查i值合法性
    while (j < i - 1 && p != NULL) {  // 查找第i-1个结点p
        j++;
        p = p->next;
    }
    if (p == NULL) return false;  // 未找到第i-1个结点
    else {  // 找到了第i-1个结点p
        s = (DLinkNode*)malloc(sizeof(DLinkNode));  // 创建新结点s
        s->data = e;
        s->next = p->next;  // 将结点s插入到结点p之后
        if (p->next != NULL) p->next->prior = s;
        s->prior = p;
        p->next = s;
        return true;
    }
}

2.5 删除(ListDelete)

删除元素同样需要定位到待删除结点的前一个结点,然后调整指针以跳过该结点。即:

代码为:

bool ListDelete(DLinkNode*& L, int i, ElemType& e)  //删除第i个元素
{
	int j = 0;
	DLinkNode* p = L, * q;			//p指向头结点,j设置为0
	if (i <= 0) return false;		//i错误返回假
	while (j < i - 1 && p != NULL)	//查找第i-1个结点p
	{
		j++;
		p = p->next;
	}
	if (p == NULL)				//未找到第i-1个结点
		return false;
	else						//找到第i-1个节p
	{
		q = p->next;				//q指向第i个结点
		if (q == NULL)			//当不存在第i个结点时返回false
			return false;
		e = q->data;
		p->next = q->next;		//从双链表中删除结点q
		if (p->next != NULL)		//若p结点存在后继结点,修改其前驱指针
			p->next->prior = p;
		free(q);				//释放q结点
		return true;
	}
}

下面操作中的步骤实际上在上面操作中均以实现,且代码注释很详细,故不再讲解。

2.6 查找 (GetElem && LocateElem)

bool GetElem(DLinkNode* L, int i, ElemType& e)	//求线性表中第i个元素值
{
	int j = 0;
	DLinkNode* p = L;
	if (i <= 0) return false;		//i错误返回假
	while (j < i && p != NULL)		//查找第i个结点p
	{
		j++;
		p = p->next;
	}
	if (p == NULL)				//没有找到返回假			
		return false;
	else						//找到了提取值并返回真
	{
		e = p->data;
		return true;
	}
}

int LocateElem(DLinkNode* L, ElemType e)	//查找第一个值域为e的元素序号
{
	int i = 1;
	DLinkNode* p = L->next;
	while (p != NULL && p->data != e)	//查找第一个值域为e的结点p
	{
		i++;						//i对应结点p的序号
		p = p->next;
	}
	if (p == NULL)					//没有找到返回0
		return(0);
	else							//找到了返回其序号
		return(i);
}

2.7 遍历(DispList)

void DispList(DLinkNode* L)		//输出线性表
{
	DLinkNode* p = L->next;
	while (p != NULL)
	{
		printf("%c ", p->data);
		p = p->next;
	}
	printf("\n");
}

2.8 销毁 (DestroyList)

void DestroyList(DLinkNode*& L)	//销毁线性表
{
	DLinkNode* pre = L, * p = pre->next;
	while (p != NULL)
	{
		free(pre);
		pre = p;					//pre、p同步后移一个结点
		p = pre->next;
	}
	free(p);
}

三、编写主函数

最后汇总上述函数,测试功能,可编写如下主函数:

int main()
{
	DLinkNode* L;
	ElemType e;
	printf("顺序表的基本运算如下:\n");
	printf("  (1)初始化顺序表L\n");
	InitList(L);
	printf("  (2)依次插入a,b,c,d,e元素\n");
	ListInsert(L, 1, 'a');
	ListInsert(L, 2, 'b');
	ListInsert(L, 3, 'c');
	ListInsert(L, 4, 'd');
	ListInsert(L, 5, 'e');
	printf("  (3)输出顺序表L:");
	DispList(L);
	printf("  (4)顺序表L长度:%d\n", ListLength(L));
	printf("  (5)顺序表L为%s\n", (ListEmpty(L) ? "空" : "非空"));
	if (GetElem(L, 3, e) == false)
		printf("  (6)顺序表L的第3个元素:查找失败\n");
	else
		printf("  (6)顺序表L的第3个元素:%c\n", e);
	printf("  (7)元素h的位置:%d\n", LocateElem(L, 'h'));
	printf("  (8)在第4个元素位置上插入f元素\n");
	if (ListInsert(L, 4, 'f') == false)
		printf("   插入失败\n");
	printf("  (9)输出顺序表L:");
	DispList(L);
	printf("  (10)删除L的第3个元素\n");
	if(ListDelete(L, 3, e)==false)
		printf("   删除失败");
	printf("  (11)输出顺序表L:");
	DispList(L);
	printf("  (12)释放顺序表L\n");
	DestroyList(L);
	return 1;
}

测试结果

正常的运行结果测试见下图:

在(6)中,当查找的元素序号不在链表中时,会输出相应信息,见下图:

在(7)中,如果查找的元素不在链表中,会输出对应的序号是0,见下图:

在(8)中,如果插入的序号不对,也会提示相应信息,即插入失败,见下图:

在(10)中,如果删除的序号不对,也会提示相应信息,即删除失败,见下图: