【单链表】
概述
线性表的链式存储方式称为链表,其通过一组任意存储单元来存储线性表中的数据元素,最基本一种链接方式构成的链表称为单链表
为了建立数据元素间的线性关系,对于每个链表结点,除了存放数据元素(数据域)外,还要存储指向下一结点的指针(指针域)
这就使得不再要求大片连续的空间来存储数据,可以利用指针来将不连续的空间建立链接关系,进一步可以对空间进行扩充
此外,由于指针的特性,在单链表中查找某个元素时,要从表头开始遍历,这被称为非随机存取
头指针与头结点
为了标识单链表,引入头指针的概念,其始终指向单链表的第一个结点
同时,在单链表第一个结点前附加一个结点,其数据域一般存储表长等信息,指针域指向单链表第一个结点,这个附加的结点被称为头结点
头结点引用的目的是为了方便编程,将空表与非空表的操作统一,无须特殊处理:
- 头结点指针域指向单链表第一个结点,使得第一个结点的操作与其他结点操作一致
- 无论单链表是否为空,头指针都指向头结点的指针
在带有头结点时,头指针会指向头结点(第 $0$ 个结点),头结点指向链表第 $1$ 个结点;在不带头结点时,头指针直接指向单链表的第 $1$ 个结点
单链表的实现类如下:
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
| template <typename T> struct LNode { T data; LNode<T> *next; }; template <typename T> class LinkList { public: LinkList(); LinkList(T data[], int len); ~LinkList(); int getLen(); bool empty(); void print(); LNode<T> *getNodeByValue(T elem); LNode<T> *getNodeByLocate(int pos); bool insertNextNode(LNode<T> *p, T elem); bool insertPreNode(LNode<T> *p, T elem); bool insertNode(int pos, T elem); bool deleteNodeByLocate(LNode<T> *p, T &elem); bool deleteNodeByElem(T elem); void headInsert(T data[], int len); void tailInsert(T data[], int len); private: LNode<T> *first; };
|
【初始化表与销毁表】
初始化与单链表的建立
对于单链表 L
来说,在初始化完毕后,即可对于给定的数据序列 data[]
来建立单链表,建立单链表有两种方式,一种是头插法,另一种是尾插法
对于带头结点的单链表 L
,在进行初始化时,需要为头指针 first
建立一个头结点,并使头结点的指向为 NULL
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <typename T> LinkList<T>::LinkList() { first = (LNode<T> *)malloc(sizeof(LNode<T>)); first->next = NULL; } template <typename T> LinkList<T>::LinkList(T data[], int len) { first = (LNode<T> *)malloc(sizeof(LNode<T>)); first->next = NULL;
this->headInsert(data, len); }
|
对于不带头结点的单链表 L
,在进行初始化时,令头指针 first
指向为 NULL
即可
1 2 3 4 5 6 7 8 9 10 11
| template <typename T> LinkList<T>::LinkList() { first = NULL; } template <typename T> LinkList<T>::LinkList(T data[], int len) { first = NULL;
this->headInsert(data, len); }
|
单链表的销毁
对于单链表 L
来说,若要将其销毁,需要从头指针 first
指向的结点开始,逐个将链表中结点的存储空间释放
1 2 3 4 5 6 7 8
| template <typename T> LinkList<T>::~LinkList() { while (first != NULL) { LNode<T> *p = first; first = first->next; free(p); } }
|
【基本操作】
求表长
对于带头结点的单链表 L
,若想求其长度,只需从头结点开始枚举,依次判断下一结点是否为 NULL
,同时,每枚举一个结点令长度加一,直到当前结点所指向的下一个结点为 NULL
为止
1 2 3 4 5 6 7 8 9 10
| template <typename T> int LinkList<T>::getLen() { int len = 0; LNode<T> *p = first; while (p->next != NULL) { p = p->next; len++; } return len; }
|
对于不带头结点的单链表 L
,若想求其长度,只需从头指针指向的第一个结点开始枚举,依次判断当前结点是否为 NULL
,同时,每枚举一个结点令长度加一,直到当前结点所指向的下一个结点为 NULL
为止
1 2 3 4 5 6 7 8 9 10
| template <typename T> int LinkList<T>::getLen() { int len = 0; LNode<T> *p = first; while (p != NULL) { p = p->next; len++; } return len; }
|
判空
对于带头结点的单链表 L
,若想判断其是否为空,只需判断 L
的头指针指向的头结点是否指向 NULL
即可
1 2 3 4 5 6
| template <typename T> bool LinkList<T>::empty() { if (first->next == NULL) return true; return false; }
|
对于不带头结点的单链表 L
,若想判断其是否为空,只需判断 L
的头指针的指向是否为 NULL
即可
1 2 3 4 5 6
| template <typename T> bool LinkList<T>::empty() { if (first == NULL) return true; return false; }
|
输出
对于带头结点的单链表 L
,若将其输出,只需从头结点指向的第一个节点开始遍历,只要结点不为空,就输出结点的数据域
1 2 3 4 5 6 7 8 9
| template <typename T> void LinkList<T>::print() { LNode<T> *p = first->next; while (p != NULL) { cout << p->data << " "; p = p->next; } cout << endl; }
|
对于不带头结点的单链表 L
,若将其输出,只需从头指针指向的第一个节点开始遍历,只要结点不为空,就输出结点的数据域
1 2 3 4 5 6 7 8 9
| template <typename T> void LinkList<T>::print() { LNode<T> *p = first; while (p != NULL) { cout << p->data << " "; p = p->next; } cout << endl; }
|
【查找】
按值查找
按值查找,即获取 L
中与给定参数值 elem
相同的结点,若查找失败,则返回 NULL
,若查找成功,则返回该结点,因此,在调用该方法时,需要进行判断是否返回值为 NULL
对于带头结点的单链表 L
,只需从头结点开始遍历,判断当前数据域是否为 elem
1 2 3 4 5 6 7
| template <typename T> LNode<T> *LinkList<T>::getNodeByValue(T elem) { LNode<T> *p = first->next; while (p != NULL && p->data != elem) p = p->next; return p; }
|
对于不带头结点的单链表 L
,只需从头指针开始遍历,判断当前数据域是否为 elem
1 2 3 4 5 6 7
| template <typename T> LNode<T> *LinkList<T>::getNodeByValue(T elem) { LNode<T> *p = first; while (p != NULL && p->data != elem) p = p->next; return p; }
|
按位查找
按位置查找,即获取 L
中第 pos
个结点,在查找前需要进行越界判断,若查找位置异常,则返回 NULL
,若查找位置正常,则返回该结点,因此,在调用该方法时,需要进行判断是否返回值为 NULL
对于带头结点的单链表 L
,之后建立一个从 $0$ 开始的累加器,记录当前指针指向的是第几个结点,当遍历到第 pos
个结点时,返回指针所指向的当前结点即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename T> LNode<T> *LinkList<T>::getNodeByLocate(int pos) { if (pos < 0) return NULL; if (pos == 0) return first;
LNode<T> *p; p = first; int now_pos = 0;
while (p != NULL && now_pos < pos) { p = p->next; now_pos++; } return p; }
|
对于不带头结点的单链表 L
,之后建立一个从 $1$ 开始的累加器,记录当前指针指向的是第几个结点,当遍历到第 pos
个结点时,返回指针所指向的当前结点即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| template <typename T> LNode<T> *LinkList<T>::getNodeByLocate(int pos) { if (pos <= 0) return NULL;
LNode<T> *p; p = first; int now_pos = 1;
while (p != NULL && now_pos < pos) { p = p->next; now_pos++; } return p; }
|
【插入】
后插
后插,是指在给定 p
结点后插入元素 elem
,一般用于按位插入,不单独使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <typename T> bool LinkList<T>::insertNextNode(LNode<T> *p, T elem) { if (p == NULL) return false;
LNode<T> *s = (LNode<T> *)malloc(sizeof(LNode<T>)); s->data = elem; s->next = p->next; p->next = s;
return true; }
|
前插
前插,是指在给定 p
结点前插入元素 elem
,由于指针机制是后向机制,若采用循环遍历的方式找到前驱结点进行前插,时间复杂度为 $O(n)$,而采用以下交换数据的方式,时间复杂度仅为 $O(1)$
前插与后插一样,一般用于按位插入,不单独使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename T> bool LinkList<T>::insertPreNode(LNode<T> *p, T elem) { if (p == NULL) return false;
LNode<T> *s = (LNode<T> *)malloc(sizeof(LNode<T>));
s->next = p->next; p->next = s;
s->data = p->data; p->data = elem; return true; }
|
按位插入
按位置插入,即在 L
中第 pos
个结点插入元素 elem
,在插入前需要进行越界判断,在插入时,可以找到第 pos-1
个结点进行后插,也可以找到第 i
个结点进行前插,但无论使用后插还是前插,都需要注意判断插入位置是否异常
对于带头结点的单链表 L
,由于头结点的存在,使得操作统一,因此直接进行插入即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename T> bool LinkList<T>::insertNode(int pos, T elem) { if (pos < 1) return false;
LNode<T> *p = getNodeByLocate(pos - 1); if (p == NULL) return false; return insertNextNode(p, elem);
}
|
对于不带头结点的单链表 L
,在插入第一个位置时,需要改变头指针 ,令其指向新的结点,其余位置则与带头结点的单链表操作相同
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
| template <typename T> bool LinkList<T>::insertNode(int pos, T elem) { if (pos < 1) return false; if (i == 1) { LNode<T> *s = (LNode<T> *)malloc(sizeof(LNode<T>)); s->data = elem; s->next = first; first = s; return true; } LNode<T> *p = getNodeByLocate(pos - 1); if (p == NULL) return false; return insertNextNode(p, elem);
}
|
【删除】
删除指定结点
在按值查找、按位查找寻找到某一指定结点 p
后,要删除该结点并返回该结点的数据域 elem
,方法如下:
- 若删除的是非最后结点,只需将要删除的结点
p
之后的结点 s
的数据存入 p
,然后再断开 p
与 s
的链接,令 p
指向 s
之后的结点,再释放空间即可
- 若删除的是最后结点,由于其没有后继结点,无法采用上述方法,只能从头到尾开始遍历,在单链表中匹配与
p
地址相同的结点,然后将该结点前的结点指向 NULL
,再释放空间
可见,若删除的是非最后结点,时间复杂度为 $O(1)$;若删除的是最后结点,时间复杂度为 $O(n)$
无论是带头结点的单链表,还是不带头结点的单链表,在该删除指定结点方法中,操作是一致的,唯一的区别仅是:当删除的是最后结点时,需要从头到尾开始遍历,带头结点的单链表,从头结点的下一结点开始,不带头结点的单链表,从头指针指向的结点开始
对于带头结点的单链表 L
,在删除指定结点时,若删除的是非最后结点,直接进行删除即可,若删除的是最后一个结点,则从头结点的下一结点开始遍历
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
| template <typename T> bool LinkList<T>::deleteNodeByLocate(LNode<T> *p, T &elem) { if (p == NULL) return false; if (p->next != NULL) { LNode<T> *s = p->next; elem = p->data; p->data = s->data; p->next = s->next; free(s); return true; }
if (p->next == NULL) { LNode<T> *s = first->next; while (s->next != NULL) { if (s->next == p) { elem = p->data; s->next = NULL; free(p);
return true; } s = s->next; } } }
|
对于不带头结点的单链表 L
,在删除指定结点时,若删除的是非最后结点,直接进行删除即可,若删除的是最后一个结点,则从头指针所指向的结点开始遍历
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
| template <typename T> bool LinkList<T>::deleteNodeByLocate(LNode<T> *p, T &elem) { if (p == NULL) return false; if (p->next != NULL) { LNode<T> *s = p->next; elem = p->data; p->data = s->data; p->next = s->next; free(s); return true; }
if (p->next == NULL) { LNode<T> *s = first; while (s->next != NULL) { if (s->next == p) { elem = p->data; s->next = NULL; free(p);
return true; } s = s->next; } } }
|
删除给定值的所有结点
对于给定值 elem
,要在单链表中删除所有数据域为 elem
的结点,删除方法如下:
- 设置一个指向工作结点
p
的前驱结点的指针 pre
- 用工作结点
p
从头到尾扫描单链表:若 p
所指结点值为 elem
,则将其删除,并让 p
指向下一结点,否则令 pre
与 p
后移一个结点
对于带头结点的单链表 L
,前驱指针一开始设为头结点 first
,工作指针设为头指针指向的下一结点 first->next
,在删除给定值的所有结点时,从头结点的下一结点开始遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <typename T> bool LinkList<T>::deleteNodeByElem(T elem) { LNode<T> *pre = first; LNode<T> *p = first->next; if (p == NULL) return false; while (p != NULL) { if (p->data == elem) { LNode<T> *s = p; p = p->next; pre->next = p; free(s); } else { pre = p; p = p->next; } } }
|
对于不带头结点的单链表 L
,在删除给定值的所有结点时,前驱指针一开始设为 NULL
,工作指针设为头指针指向的结点 first
,在删除给定值的所有结点时,从头指针指向的结点开始遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template <typename T> bool LinkList<T>::deleteNodeByElem(T elem) { LNode<T> *pre = NULL; LNode<T> *p = first; if (p == NULL) return false; while (p != NULL) { if (p->data == elem) { LNode<T> *s = p; p = p->next; pre->next = p; free(s); } else { pre = p; p = p->next; } } }
|
【头插法与尾插法】
头插法
头插法,是每次将新申请的结点插入到链表最开始的位置,举例来说,对于一个序列 $\{1,2,3,4,5\}$,在使用头插法建立单链表后,单链表从头到尾每个结点的数据域依次为 $\{5,4,3,2,1\}$,因此,其常用于链表的建立、链表的逆置
可以发现,对于带头结点的单链表来说,每次插入都是对头结点进行后插操作
1 2 3 4 5
| template <typename T> void LinkList<T>::headInsert(T data[], int len) { for (int i = 0; i < len; i++) insertNextNode(first, data[i]); }
|
对于不带头结点的单链表来说,其实质是建立后继关系,通过不断交换头指针与新结点的地址来实现头插
1 2 3 4 5 6 7 8 9
| template <typename T> void LinkList<T>::headInsert(T data[], int len) { for (int i = 0; i < len; i++) { LNode<T> *p = (LNode<T> *)malloc(sizeof(LNode<T>)); p->data = data[i]; p->next = first; first = p; } }
|
尾插法
尾插法,是每次将新申请的结点插到链表的最后面,由于指针机制是后向机制,每次插入都需要从头指针开始遍历到表尾,为减少时间复杂度,尾插法引入了尾指针 tail
,其指向单链表的表尾结点,使得每次插入都能让新结点处于最后的位置
对于带头结点的单链表来说,只需要利用尾指针 tail
,不断地申请新结点,填充数据域后建立后继关系即可
1 2 3 4 5 6 7 8 9 10 11 12
| template <typename T> void LinkList<T>::tailInsert(T data[], int len) { LNode<T> *tail = first; for (int i = 0; i < len; i++) { LNode<T> *p = (LNode<T> *)malloc(sizeof(LNode<T>)); p->data = data[i];
tail->next = p; tail = p; } tail->next = NULL; }
|
对于不带头结点的单链表来说,由于初始头指针指向为空,因此要先建立一个初始结点,将头指针指向该结点,再对剩下的 n-1
个元素建立结点,填充数据域建立后继关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| template <typename T> void LinkList<T>::tailInsert(T data[], int len) { LNode<T> *tail = first; LNode<T> *p = (LNode<T> *)malloc(sizeof(LNode<T>)); p->data = data[0]; p->next = first; first = p; tail = first;
for (int i = 1; i < len; i++) { LNode<T> *p = (LNode<T> *)malloc(sizeof(LNode<T>)); p->data = data[i];
tail->next = p; tail = p; } tail->next = NULL; }
|