为了弥补和克服上一节所述顺序存储结构所带来的的不足,这一节讨论线性表的另一种存储结构——链式存储结构。链式存储结构不要求逻辑上相邻的数据元素在物理位置上也相邻,通过 指针 来映射数据元素之间的逻辑关系。
使用连式存储结构时,每个数据元素除了存储自身的数据信息外,还需要存储一个指示其直接后继元素位置的信息,这两部分组成一个 链结点。
当链表中每一个链结点除了数据域外只设置一个指针域时,称这样的链表为 线性链表 或 单链表。
C 语言实现
链结点
一个链结点定义如下:
typedef struct node {
ElemType data;
struct node *link;
} LNode, *LinkList;
基本操作
线性链表的基本操作都比较简单,挑几个重点说明如下。
创建链表
书中没有指定每个数据元素的来源,这里假设是由用户输入的,使用 scanf 函数。
/**
* 1. 建立一个线性链表
*/
LinkList create(int n) {
LinkList list = NULL, rear = NULL;
for (int i = 0; i < n; i++) {
int val;
scanf("%d", &val);
LNode *node = malloc(sizeof(LNode));
node -> data = val;
node -> link = NULL;
if (list == NULL) {
list = node;
} else {
rear -> link = node;
}
rear = node;
}
return list;
}
需要注意一下如果使用的是判断 rear == NULL,那么 rear 在声明的时候必须赋初值为 NULL,否则 rear 初始化时是一个垃圾值,不会进入判断条件,使得下面调用 rear -> link 会抛出异常。
插入结点
插入分了三种,往头部插入、往尾部插入以及在某个结点后插入。后面两种比较简单,但注意第一种。C 语言是 按值传递,如果此处函数签名不是 LinkList 的指针(LinkList*),而直接使用 LinkList,那么在最后赋值时,list = newNode 是不会起作用的,因为修改的只是 形参的指针。
/**
* 5. 在非空线性链表第一个链结点前插入一个 item
*
* 注意:签名必须是指针的指针
*/
void insertLink1(LinkList *list, ElemType item) {
LinkList newNode = malloc(sizeof(LNode));
newNode -> data = item;
newNode -> link = *list;
*list = newNode;
}
/**
* 6. 在非空线性链表的末尾插入一个 item
*/
void insertLink2(LinkList list, ElemType item) {
LinkList rear = list;
while (rear -> link != NULL) {
rear = rear -> link;
}
LinkList newNode = malloc(sizeof(LNode));
newNode -> data = item;
newNode -> link = NULL;
rear -> link = newNode;
}
/**
* 7. 在线性链表指针 p 后面插入一个 item
*/
void insertLink3(LinkList list, LinkList q, ElemType item) {
LinkList newNode = malloc(sizeof(LNode));
newNode -> data = item;
if (list == NULL) {
newNode -> link = NULL;
list = newNode;
} else {
newNode -> link = q -> link;
q -> link = newNode;
}
}
经典算法题
线性链表的反转
线性链表的反转是一个比较经典的面试题,实际上实现起来也比较简单,主要需要理清思路。
实际上需要维护的指针有三个:
- 当前遍历到的链结点(
cur) - 上一个链结点(
prev) - 上上个链结点(
prevPrev)
当每一步进行反转时,实际上反转的是将 prev 指向 prevPrev,因为 cur 指针的 link 是不能动的,否则你就找不到下一个链结点了。
所以思路是(while 循环中的四行代码):
- 上上个结点(
prevPrev)往后走 - 上个结点(
prev)往后走 - 当前结点(
cur)往后走 - 反转
prev与prevPrev
/**
* 13. 线性链表的反转
*/
void invert(LinkList *list) {
LinkList cur = *list, prev = NULL, prevPrev = NULL;
while (cur != NULL) {
prevPrev = prev;
prev = cur;
cur = cur -> link;
prev -> link = prevPrev;
}
*list = prev;
}
删除链表的倒数第 N 个节点
因为链表是单向的,无法直接从后往前寻找倒数第 N 个节点。要使时间复杂度为 O(n),即一次遍历实现,可以基于如下思路:
使用两个指针,其中一个指针比另一个领先 N 个节点,然后两个节点同时往后移动,当前面的指针到达结尾时,后面的指针则到达了倒数第 N 个位置。
struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
struct ListNode* pioneer = head;
struct ListNode* prev = head;
for (int i = 0; i < n; i++) {
pioneer = pioneer -> next;
}
if (pioneer == NULL) {
prev = prev -> next;
return prev;
}
while (pioneer -> next != NULL) {
prev = prev -> next;
pioneer = pioneer -> next;
}
prev -> next = prev -> next -> next;
return head;
}