数据结构4:双向链表
前情提要
上次我们讲到了局部有环的单链表。

循环链表:找出入口和长度
现在需要你找出一个循环链表的环入口。
我们的步骤是:
- 设定快指针的速度是慢指针的两倍。
- 当快慢指针相遇时,慢指针已经走了 k 步,快指针走了 2k 步。
- 当快慢指针相遇时,我们已经进到了环中
- 现在设立一个新指针指向快指针,使其遍历并记录步数直到与快慢指针相遇,步数就是环长度。
- 初始化快慢指针为头指针,然后让快指针前进【环长度】步
- 让快慢指针以相同速度前进,直到相遇,相遇点就是入口
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void findroundBegin (Node *head){ Node *fast = head; Node *slow = head; while(fast->next!= NULL && fast->next->next != NULL){ fast = fast->next->next; slow = slow->next; if(fast == slow){ Node *p = fast; int count = 0; while(p-> next != slow){ count++; p = p->next; } fast = head; slow = head; for(int i = 0; i < count; i++){ fast = fast->next; } while(fast != slow){ fast = fast->next; slow = slow->next; } printf("环的入口为:%d\n", fast->data); return slow; } }
|
双向链表
链式存储结构的节点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他节点。若要寻查结点的直接前驱、则必须从表头指针出发。换句话说,在单链表中,查找直接后继的执行时间为O(1),而查找直接前驱的执行时间为O(n)。
为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。在双向链表的节点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
节点示例:
1 2 3 4
| typedef struct Node{ int data; struct Node 8prev,*next; }Node;
|
这就生成了一个节点,该节点有一个数据域和两个指针域。
头插法

类似地,插入一个新节点,我们的步骤如下:
- 使得新节点的prev指针指向头指针,next指针指向头指针的后一个节点。
- 使头节点的后一个节点的prev指针指向新节点
- 使头节点的next指针指向新节点
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void insert_head(Node *head,int data) { Node *new_node = (Node*)malloc(sizeof(Node)); new_node->data = data; new_node->prev = head; new_node->next = head->next; if (head->next != NULL) { head->next->prev = new_node; } head->next = new_node; return 1;
}
|
尾插法
这个相对于头插法的步骤更简单,但是时间复杂度会比较高:
- 找到尾节点,把尾节点的next指针指向新节点
- 把新节点的prev指针指向尾节点,next指针指向Null
代码实现:
1 2 3 4 5 6 7 8 9
| Node* insertTail(Node *tail, ElemType e) { Node *p = (Node*)malloc(sizeof(Node)); p->data = e; p->prev = tail; tail->next = p; p->next = NULL; return p; }
|
指定位置插入节点
类似于单链表的插入,移动到指定点,然后插入。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void insertAtPosition(Node** head, int position, int value) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = value; newNode->prev = NULL; newNode->next = NULL;
if (*head == NULL) { *head = newNode; return; }
if (position == 0) { newNode->next = *head; (*head)->prev = newNode; *head = newNode; return; }
Node* current = *head; int count = 0; while (current != NULL && count < position - 1) { current = current->next; count++; }
if (current == NULL) { printf("插入位置超出链表长度\n"); free(newNode); return; }
newNode->next = current->next; if (current->next != NULL) { current->next->prev = newNode; } current->next = newNode; newNode->prev = current; }
|
双向链表和顺序表的对比
比较项目 |
存储结构 |
顺序表 |
链表 |
|
存储空间 |
预先分配,会出现闲置或溢出现象 |
动态分配,不会出现存储空间闲置或溢出现象 |
空间 |
存储密度 |
不用为表示节点间的逻辑关系而增加额外的存储,存储密度等于 1 |
需要借助指针来体现元素间的逻辑关系,存储密度小于 1 |
|
存取元素 |
随机存取,按位置访问元素的时间复杂度为 O(1) |
顺序存取,按位置访问元素时间复杂度为 O(n) |
时间 |
插入、删除 |
平均移动约表中一半元素,时间复杂度为 O(n) |
不需要移动元素,确定插入、删除位置后,时间复杂度为 O(1) |
|
适用情况 |
1) 表长变化不大,且能事先确定变化的范围2) 很少进行插入或删除操作,经常按元素位置序号访问数据元素 |
1) 长度变化较大2) 频繁进行插入或删除操作 |