C/C++数据结构之双向链表
概述
双向链表由一系列节点组成,每个节点不仅包含数据域,还包含指向前一个节点和后一个节点的引用。与单向链表相比,双向链表允许我们从任意一个节点出发,既能够向前遍历也能向后遍历,这使得某些操作可以更加高效。
想象一下一列火车,每节车厢就像双向链表中的一个节点。每节车厢都有前后两个连接点,通过这些连接点,车厢相互连接形成整列火车。我们可以从任意一节车厢出发,向前或向后移动到相邻的车厢,这与双向链表支持双向遍历的特点非常相似。
声明与初始化
在C/C++中实现双向链表时,我们需要定义一个节点结构体。该结构体至少应包含三个部分:前驱指针、数据域和后继指针。前驱指针用于指向当前节点的直接前驱,而后继指针则指向当前节点的直接后继。
在下面的示例代码中,Node结构体包含了三个成员:nData用于存放整型数据;pPrev是一个指向Node类型的指针,用于链接上一个节点;pNext也是一个指向Node类型的指针,用于链接下一个节点。
struct Node
{int nData; // 数据域struct Node* pPrev; // 前驱指针,指向上一个节点struct Node* pNext; // 后继指针,指向下一个节点
};
双向链表的初始化通常涉及到创建一个新的节点,并将其设置为整个链表的头节点。我们可以编写一个函数来创建并初始化一个新的节点,编写另一个函数以在链表头部插入节点,具体实现可参考下面的示例代码。
Node* CreateNode(int nData)
{Node *pNode = new Node();memset(pNode, 0, sizeof(Node));pNode->nData = nData;return pNode;
}void InsertAtHead(Node** pHead, Node* pNode)
{if (*pHead == NULL){// 若链表为空,直接赋值*pHead = pNode;}else{// 新节点的next指向原头节点pNode->pNext = *pHead;// 原头节点的prev指向新节点(*pHead)->pPrev = pNode;// 更新头指针*pHead = pNode;}
}int main()
{Node* pHead = NULL;// 创建一些新节点,并插入到双向链表中InsertAtHead(&pHead, CreateNode(66));InsertAtHead(&pHead, CreateNode(99));return 0;
}
基本操作
在双向链表中,最基本的操作包括:遍历节点、插入节点和删除节点。
遍历节点
遍历双向链表有两种方式:从前向后遍历和从后向前遍历。具体如何实现,可参考下面的示例代码。
void TraverseForward(Node* pNode)
{Node* pTemp = pNode;while (pTemp != NULL){printf("%d ", pTemp->nData);pTemp = pTemp->pNext;}printf("\n");
}void TraverseBackward(Node* pNode)
{Node* pTemp = pNode;while (pTemp != NULL){printf("%d ", pTemp->nData);pTemp = pTemp->pPrev;}printf("\n");
}
插入节点
插入操作可以分为以下三种情况:在链表头部插入、在链表尾部插入、在指定位置之前插入。
在链表头部插入节点,前面已经介绍过了,这里不再赘述。在链表尾部插入节点相对简单,只需找到链表的最后一个节点,并将它的pNext指针指向新节点,同时设置新节点的pPrev指针指向最后一个节点。
void InsertAtTail(Node** pHead, Node* pNode)
{if (*pHead == NULL){// 若链表为空*pHead = pNode;}else{Node* pTemp = *pHead;// 找到最后一个节点while (pTemp->pNext != NULL){pTemp = pTemp->pNext;}pTemp->pNext = pNode;pNode->pPrev = pTemp;}
}
在指定位置之前插入节点,需要先找到目标位置的前一个节点,然后调整前后节点之间的连接关系。
void InsertBefore(Node* pTarget, Node* pNode)
{if (pTarget == NULL || pTarget->prev == NULL || pNode == NULL){return;}Node* pPrevNode = pTarget->pPrev;pPrevNode->pNext = pNode;pNode->pPrev = pPrevNode;pNode->pNext = pTarget;pTarget->pPrev = pNode;
}
删除节点
根据待删除节点的位置不同,删除操作可以分为:删除头节点、删除尾节点、删除中间节点。
在下面的示例代码中,我们根据删除的位置,进行了不同的处理。如果要删除的节点是当前链表的头节点,则将头指针指向下一个节点;如果新的头节点不为空,则将其前驱指针设置为NULL,因为它是新的第一个节点,没有前驱。如果不是头节点,则将前驱节点的pNext指向目标节点的后继节点;如果目标节点有后继节点,将其pPrev指向前驱节点,从而将目标节点从链表中移除。
void DeleteNode(Node** pHead, Node* pNode)
{if (pHead == NULL || pNode == NULL){return;}// 如果要删除的是头节点if (pNode == *pHead){*pHead = pNode->pNext;// 更新头指针if (*pHead != NULL){(*pHead)->pPrev = NULL;}}else{// 中间或尾部节点,先调整前驱节点的pNextif (pNode->pPrev != NULL){pNode->pPrev->pNext = pNode->pNext;}// 如果不是尾节点,则需要设置后继节点的pPrevif (pNode->pNext != NULL){pNode->pNext->pPrev = pNode->pPrev;}}delete pNode;pNode = NULL;
}
总结
双向链表的优势主要在于:灵活性和高效性。在需要频繁进行插入和删除操作的应用场景中,双向链表通常比数组更优,因为不需要移动其他元素以保持连续性。另外,由于可以双向遍历,查找特定元素或执行反向遍历时也更为便捷。