数据结构之线性表(2) 选择题 01. 关于线性表的顺序存储结构和链式存储结构的描述中,正确的是()。
I.线性表的顺序存储结构优于其链式存储结构
II.链式存储结构比顺序存储结构能更方便地表示各种逻辑结构
III.若频繁使用插入和删除结点操作,则顺序存储结构更优于链式存储结构
IV.顺序存储结构和链式存储结构都可以进行顺序存取
解析:
I 错误:两种存储结构各有适用场合,不能简单比较优劣。
II 正确:链式存储结构能够更方便地表示各种逻辑结构。
III 错误:顺序存储结构在频繁插入和删除时效率低下,因为需要移动大量元素。
IV 正确:顺序存储结构支持随机存取和顺序存取。
02. 对于一个线性表,既要求能够进行较快速地插入和删除,又要求存储结构能反映数据之间的逻辑关系,则应该用()。
A. 顺序存储方式
B. 链式存储方式
C. 散列存储方式
D. 以上均可以
答案:B
解析:
链式存储方式能够方便地表示逻辑关系,并且插入和删除的时间复杂度为 $ O(1) $。
03. 对于顺序存储的线性表,其算法时间复杂度为 $ O(1) $ 的运算应该是()。
A. 将n个元素从小到大排序
B. 删除第i个元素
C. 改变第i个元素
D. 在第i个元素后插入一个新元素
答案:C
解析:
选项A的时间复杂度至少为 $ O(n \log n) $。
选项B和D也需要移动元素,因此时间复杂度为 $ O(n) $。
04. 下列关于线性表说法中,正确的是()。
A. 顺序存储方式只能用于存储线性结构
B. 取线性表的第i个元素的时间与i的大小有关
C. 静态链表需要分配较大的连续空间,插入和删除不需要移动元素
D. 在一个长度为n的有序单链表中插入一个新结点并仍保持有序的时间复杂度为 $ O(n) $
E. 若用单链表来表示队列,则应该选用带尾指针的循环链表
解析:
A错误:顺序存储方式也适用于图和树。
B错误:取线性表的第i个元素的时间复杂度为 $ O(1) $。
C正确:静态链表不需要大块连续空间,但插入和删除仍需调整指针。
D正确:在有序单链表中,插入新节点的时间复杂度确实为 $ O(n) $。
E正确:使用带尾指针的循环链表更方便实现队列,插入和删除的复杂度都是$O(1)$。
05. 线性表中有 2n 个元素,在单链表上实现要比在顺序表上实现效率更高,正确的是()。
A. 删除所有值为x的元素
B. 在最后一个元素的后面插入一个新元素
C. 顺序输出前k个元素
D. 交换第i个元素和第 2n-i-1个元素的值(i=0…,n-1)
答案:A
解析:
A选项在单链表上实现不需要移动元素,因此效率更高。
B和C在顺序表中效率更高。
D需要访问和交换元素,顺序表的实现更优。
06. 在一个单链表中,已知q所指结点是p所指结点的前驱结点,若在q和p之间插入结点s,则执行()。
A. $ s->next = p->next; p->next = s; $
B. $ p->next = s->next; s->next = p; $
C. $ q->next = s; s->next = p; $
D. $ p->next = s; s->next = q; $
答案:C
解析:
当在结点q和结点p之间插入结点s时,q的next指向s,s的next指向p,形成了新的连接。因此,选项C是正确的。
07. 给定有n个元素的一维数组,建立一个有序单链表的最低时间复杂度是()。
A. $ O(1) $
B. $ O(n) $
C. $ O(n^2) $
D. $ O(n \log n) $
答案:D
解析:
若先建立链表,然后依次插入元素以建立有序表,每次插入一个元素都需遍历链表寻找插入位置,因此时间复杂度为 $ O(n) $,总的时间复杂度为 $ O(n^2) $。如果是先将数组排序再建立链表,时间复杂度为 $ O(n \log n) $(排序)加上 $ O(n) $(链表构建),所以总时间复杂度为 $ O(n \log n) $。
08. 将长度为 n 的单链表链接在长度为 m 的单链表后面,其算法的时间复杂度采用大 O 形式表示应该是( )。
A. $ O(1) $
B. $ O(n) $
C. $ O(m) $
D. $ O(n + m) $
答案:C
解析:
为了将长度为 n 的单链表连接到长度为 m 的单链表后面,首先需要遍历长度为 m 的单链表找到尾结点,其时间复杂度为 $ O(m) $。
09. 单链表中,增加一个头结点的目的是()。
A. $ O(1) $
B. 标识表结点中首结点的位置
C. 方便运算的实现
D. 说明单链表是线性表的链式存储
答案:C
解析:
设置头结点的主要目的是为了方便运算的实现。通过引入头结点,可以统一处理链表的插入和删除操作,不论链表是否为空,头指针始终指向头结点,简化了对链表操作的逻辑。
10. 在一个长度为 n 的带头结点的单链表 h 上,设有尾指针 r,则执行( )操作与链表的表长有关。
A. 删除单链表中的第一个元素
B. 删除单链表中的最后一个元素
C. 在单链表第一个元素前插入一个新元素
D. 在单链表最后一个元素后插入一个新元素
答案:B
解析:
删除单链表的最后一个结点需要找到其前驱结点,并将其指针域置为 NULL。这需要从头结点开始遍历链表,时间复杂度为 $ O(n) $,因此该操作与链表的表长有关。其他操作(如删除第一个元素、在第一个元素前插入新元素或在最后一个元素后插入新元素)都是常数时间操作。
11. 对于一个头指针为 head 的带头结点的单链表,判定该表为空表的条件是( );对于不带头结点的单链表,判定空表的条件为()。
A. $ head == NULL $
B. $ head->next == NULL $
C. $ head->next == head $
D. $ head != NULL $
答案:B,A
解析:
在带头结点的单链表中,头指针指向头结点,而头结点的next域指向第一个元素。如果 $ head->next == NULL $,则表示该单链表为空。
在不带头结点的单链表中,如果 $ head == NULL $,则表示该单链表为空。
12. 下面关于线性表的一些说法中,正确的是()。
A. 对一个设有头指针和尾指针的单链表执行删除最后一个元素的操作与链表长度无关
B. 线性表中每个元素都有一个直接前驱和一个直接后继
C. 为了方便插入和删除数据,可以使用双链表存放数据
D. 取线性表第 i 个元素的时间与 i 的大小有关
答案:C
解析:
选项A是错误的,因为删除最后一个元素需要找到其前驱结点,时间复杂度与链表长度有关。
选项B也是错误的,因为线性表的第一个元素没有前驱,最后一个元素没有后继。
选项C是正确的,双链表可以方便地访问前驱和后继,适合插入和删除操作。
选项D不正确,因为在顺序存储情况下取第 i 个元素的时间是 $ O(1) $,而在链式存储情况下,取第 i 个元素的时间复杂度为 $ O(i) $,与 i 的大小有关,但并不适用于所有情况。
13. 在双链表中向 p 所指的结点之前插入一个结点 q 的操作为()。
A. $ p->prior = q; q->next = p; p->prior->next = q; q->prior = p->prior; $
B. $ q->prior = p->prior; p->prior->next = q; q->next = p; p->prior = q; $
C. $ q->next = p; p->next = q; q->prior->next = q; q->next = p; $
D. $ p->prior->next=q;q->next=p;q->prior=p->prior;p->prior=q $
答案:B,D
解析:
在双链表中向p之前插入q时,首先需要设置q的前驱指针指向p的前驱结点,然后将p的前驱结点的next指针指向q,接着将q的next指针指向p,最后将p的前驱指针指向q。选项B,D正确,满足这个条件。
14. 在双向链表存储结构中,删除 p 所指的结点时必须修改指针()。
A. $ p->llink->rlink = p->rlink; p->rlink->llink = p->llink; $
B. $ p->llink = p->llink->llink; p->llink->rlink = p; $
C. $ p->rlink->llink = p; p->rlink = p->rlink->rlink; $
D. $ p->rlink = p->llink->llink; p->llink = p->rlink->rlink; $
答案:A
解析:
删除p所指的结点时,需要将p的前驱结点的右链指向p的后继结点,同时将p的后继结点的左链指向p的前驱结点,以保持双向链表的连接完整。选项A正确地描述了这个过程。
15. 在长度为 n 的有序单链表中插入一个新结点,并仍然保持有序的时间复杂度是( )。
A. $ O(1) $
B. $ O(n) $
C. $ O(m) $
D. $ O(n \log n) $
答案:B
解析:
在有序单链表中插入新结点,首先需要找到合适的位置,这个过程需要遍历链表,时间复杂度为 $ O(n) $。插入操作本身是常数时间操作,时间复杂度为 $ O(1) $。因此,总的时间复杂度为 $ O(n) $。
16. 与单链表相比,双链表的优点之一是()。
A. 插入、删除操作更方便
B. 可以进行随机访问
C. 可以省略表头指针或表尾指针
D. 访问前后相邻结点更灵活
答案:D
解析:
双链表在每个结点中都有前驱和后继指针,因此可以方便地访问相邻结点,特别是在进行插入和删除操作时,能够更灵活地修改指针。选项A并不完全正确,因为双链表在指针的修改上更复杂;选项B是错误的,因为双链表仍然不支持随机访问;选项C不成立,因为双链表同样需要表头或表尾指针。
17. 带头结点的双循环链表为空的条件是()。
A. $ L->prior == L$ && $L->next == NULL $
B. $ L->prior == NULL$ &&$ L->next == NULL $
C. $ L->prior == NULL$ &&$L->next = L $
D. $ L->prior = L $&& $L->next == L $
答案:D
解析:
在双循环链表中,头结点的前驱指针和后继指针都指向自身(即头结点自己),因此在判断空链表时,条件应为 $ L->prior == L $ 且 $ L->next == L $。选项D正确地描述了这个条件。
18. 一个链表最常用的操作是在末尾插入结点和删除结点,则选用()最节省时间。
A. 带头结点的双循环链表
B. 单循环链表
C. 带尾指针的单循环链表
D. 单链表
答案:C
解析:
带尾指针的单循环链表在末尾插入和删除结点时,不需要遍历整个链表来找到尾结点及其前驱结点,因此时间复杂度为 $ O(1) $。其他选项(如单链表和单循环链表)在末尾插入和删除时都需要先找到尾结点,时间复杂度为 $ O(n) $。
19. 设对 $ n (n > 1) $ 个元素的线性表的运算只有4种: 删除第一个元素; 删除最后一个元素; 在第一个元素之前插入新元素; 在最后一个元素之后插入新元素,则最好使用()。
A. 只有尾结点指针没有头结点指针的循环单链表
B. 只有尾结点指针没有头结点指针的非循环双链表
C. 只有头结点指针没有尾结点指针的循环双链表
D. 既有头结点指针又有尾结点指针的循环单链表
答案:C
解析:
使用循环双链表,能够方便地在头部和尾部进行插入和删除操作。由于有前驱和后继指针,可以直接访问第一个和最后一个元素,同时插入和删除的时间复杂度均为 $ O(1) $。
20. 一个链表最常用的操作是在最后一个元素后插入一个元素和删除第一个元素,则选用()最节省时间。
A. 不带头结点的单循环链表
B. 双链表
C. 不带头结点且有尾指针的单循环链表
D. 单链表
答案:C
解析:
不带头结点且有尾指针的单循环链表允许在尾部插入元素时直接访问尾结点,因此时间复杂度为 $ O(1) $。删除第一个元素也很高效,时间复杂度为 $ O(1) $,因此选项C是最优解。
21. 静态链表中指针表示的是()。
A. 下一元素的地址
B. 内存储器地址
C. 下一个元素在数组中的位置
D. 左链或右链指向的元素的地址
答案:C
解析:
静态链表中的指针称为游标,表示下一个元素在数组中的位置,而不是直接指向内存地址。
22. 需要分配较大空间,插入和删除不需要移动元素的线性表,其存储结构为()。
A. 单链表
B. 静态链表
C. 顺序表
D. 双链表
答案:B
解析:
静态链表使用数组表示,插入和删除操作不需要移动其他元素,因此适合分配较大空间并实现高效操作。
23. 某线性表用带头结点的循环单链表存储,头指针为 head,当 $ head->next->next = head $ 成立时,线性表长度可能是()。
A. 0
B. 1
C. 2
D. 可能为0或1
答案:D
解析:
如果链表为空,$ head->next $ 指向自身,条件成立;如果链表中有一个元素,$ head->next $ 指向该元素,该元素的下一个指针指向头结点,因此也符合条件。故选D。
24. 【2016 统考真题】已知一个带有表头结点的双向循环链表,结点结构为 $ |prev|data|next| $。现要删除指针 $ p $ 所指的结点,正确的语句序列是()。
A. $ p->next->prev = p->prev; p->prev->next = p->prev; free(p); $
B. $ p->next->prev = p->next; p->prev->next = p->next; free(p); $
C. $ p->prev->next = p->prev; free(p); p->next->prev = p->next; $
D. $ p->next->prev = p->prev; p->prev->next = p->next; free(p); $
答案:D
解析:
删除双向链表中的结点需要正确调整前驱和后继结点的指针。选项D正确地将指针链接更新为前驱和后继结点,确保链表的完整性,并最终释放结点。
25. 【2016 统考真题】已知表头元素为c的单链表在内存中的存储状态如下表所示。
地址
元素
链接地址
1000H
a
1010H
1004H
b
100CH
1008H
c
1000H
100CH
d
NULL
1010H
e
1004H
1014H
-
现将f存放于1014H处并插入单链表,若f在逻辑上位于a和e之间,则a、e、f的“链接地址”依次是()。
A. 1010H, 1014H, 1004H
B. 1010H, 1004H, 1014H
C. 1014H, 1010H, 1004H
D. 1014H, 1004H, 1010H
答案:D
解析:
根据给定的链表结构,插入f后,a的链接地址指向f(1014H),f的链接地址指向e(1004H),而e的链接地址指向b(1000H)。因此,a、e、f的“链接地址”依次是:1014H(f)、1004H(e)、1010H(b)。
26. 【2021统考真题】已知头指针h指向一个带头结点的非空单循环链表,结点结构为 $ data_next $,其中 next 是指向直接后继结点的指针,p是尾指针,q 是临时指针。现要删除该链表的第一个元素,正确的语句序列是()。
A. $ h->next=h->next->next; q=h->next; free(q); $
B. $ q=h->next; h->next=h->next->next; free(q); $
C. $ q=h->next; h->next=q->next; \text{if}(p != q) p = h; free(q); $
D. $ q=h->next; h->next=q->next; \text{if}(p == q) p = h; free(q); $
答案:D
解析:
要删除非空单循环链表的第一个元素,需要先用临时指针q指向待删结点,然后将q从链表中断开,更新头指针的next指向第二个元素。若待删结点是链表的尾结点(即只有一个元素),则尾指针p需要指向头结点。因此,选项D正确考虑了这一特殊情况,并确保在删除结点后适当更新尾指针。
解答题 问题01:设计一个递归算法,删除不带头结点的单链表中所有值为x的结点。 解答 我们可以使用递归来遍历链表,并删除所有值为x的结点。算法的核心思路是:对于每个结点,检查它的值是否为x。如果是,则删除这个结点,并递归调用函数以处理剩余的链表;如果不是,则继续递归处理下一个结点。
递归模型 设 $ f(L, x) $ 的功能是删除以 $ L $ 为首结点的单链表中所有值等于x的结点。我们可以推导出以下递归模型:
终止条件 :如果链表为空(即 $ L == NULL $),则什么都不做,直接返回。
递归主体 :
如果当前结点的值等于x,删除该结点,并递归调用 $ f(L->next, x) $ 处理剩余链表。
如果当前结点的值不等于x,继续递归调用 $ f(L->next, x) $ 处理下一个结点。
代码实现 以下是递归删除单链表中所有值为x的结点的C++代码实现:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; void DelX (LNode *&L, int x) { if (L == NULL ) { return ; } if (L->data == x) { LNode *p = L; L = L->next; delete p; DelX (L, x); } else { DelX (L->next, x); } } void PrintList (LNode *L) { while (L != NULL ) { cout << L->data << " " ; L = L->next; } cout << endl; } int main () { LNode *head = new LNode{1 , nullptr }; head->next = new LNode{2 , nullptr }; head->next->next = new LNode{3 , nullptr }; head->next->next->next = new LNode{2 , nullptr }; head->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head); int x = 2 ; DelX (head, x); cout << "删除值为 " << x << " 的结点后:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
答案和解释
答案 :代码正确实现了删除单链表中所有值为x的结点,算法的时间复杂度为 $ O(n) $,空间复杂度为 $ O(n) $(递归调用的深度)。
解释 :
该算法使用了递归的方式处理链表,能够直接在原链表上进行操作,不会造成断链。
递归出口确保当链表为空时结束递归。
在删除结点时,通过更新当前结点的指针来维持链表的完整性,避免了内存泄漏。
问题02. 在带头结点的单链表中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。 解答 有两种解法来实现此操作。
解法一:使用前驱指针 该方法使用指针 p
从头到尾扫描单链表,同时使用 pre
指向当前结点 p
的前驱。当 p
指向的结点值为 x
时,删除该结点并让 p
指向下一个结点;如果不是,则同时移动 pre
和 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; void DelX1 (LNode *L, int x) { LNode *p = L->next; LNode *pre = L; LNode *q; while (p != NULL ) { if (p->data == x) { q = p; p = p->next; pre->next = p; delete q; } else { pre = p; p = p->next; } } } void PrintList (LNode *L) { while (L != NULL ) { cout << L->data << " " ; L = L->next; } cout << endl; } int main () { LNode *head = new LNode{0 , nullptr }; head->next = new LNode{1 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{3 , nullptr }; head->next->next->next->next = new LNode{2 , nullptr }; head->next->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head->next); int x = 2 ; DelX1 (head, x); cout << "删除值为 " << x << " 的结点后:" ; PrintList (head->next); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
解法二:使用尾插法重建链表 该方法使用指针 p
扫描链表,将所有值不为 x
的结点链接到一个新的链表中,并释放值为 x
的结点。
代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void DelX2 (LNode *&L, int x) { LNode *p = L->next; LNode *r = L; LNode *q; while (p != NULL ) { if (p->data != x) { r->next = p; r = p; p = p->next; } else { q = p; p = p->next; delete q; } } r->next = NULL ; }
时间复杂度和空间复杂度
时间复杂度 :两种解法均为 $ O(n) $,其中 $ n $ 为链表中结点的数量,因为每个结点最多被访问一次。
空间复杂度 :两种解法均为 $ O(1) $,因为使用的额外空间是常量级别的,主要是指针变量。
问题03. 设工为带头结点的单链表,编写算法实现从尾到头反向输出每个结点的值。 解答 有多种方法可以实现从尾到头反向输出单链表中的每个结点的值,以下是三种常见的解法:
链表逆置法 :改变链表的方向,然后从头到尾输出。
使用栈 :将每个结点的值压入栈中,最后从栈顶开始输出。
使用递归 :通过递归调用实现从尾到头输出。
我们主要讨论最后两种方法,尤其是使用递归的实现方式。
方法 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; void R_Print (LNode *L) { if (L != NULL ) { R_Print (L->next); if (L->next != NULL ) { cout << L->data << " " ; } } } void PrintList (LNode *L) { while (L != NULL ) { cout << L->data << " " ; L = L->next; } cout << endl; } int main () { LNode *head = new LNode{0 , nullptr }; head->next = new LNode{1 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{3 , nullptr }; head->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head->next); cout << "反向输出链表的结点值:" ; R_Print (head->next); cout << endl; while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
方法 2:使用栈 该方法通过栈来存储链表中的每个结点的值。遍历完整个链表后,从栈顶开始输出结点的值。
代码实现: 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 45 46 47 48 49 50 51 #include <iostream> #include <stack> using namespace std;struct LNode { int data; LNode *next; }; void Stack_Print (LNode *L) { stack<int > s; LNode *p = L->next; while (p != NULL ) { s.push (p->data); p = p->next; } while (!s.empty ()) { cout << s.top () << " " ; s.pop (); } } int main () { LNode *head = new LNode{0 , nullptr }; head->next = new LNode{1 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{3 , nullptr }; head->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head->next); cout << "反向输出链表的结点值(使用栈):" ; Stack_Print (head); cout << endl; while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
时间复杂度和空间复杂度
时间复杂度 :两种方法均为 $ O(n) $,其中 $ n $ 为链表中结点的数量,因为每个结点最多被访问一次。
空间复杂度 :
递归方法:递归调用的深度最多为 $ O(n) $,因此空间复杂度为 $ O(n) $。
栈方法:同样需要 $ O(n) $ 的空间来存储结点值。
问题04. 试编写在带头结点的单链表中删除一个最小值结点的高效算法(假设最小值结点是唯一的)。 解答 本算法的基本思想是使用两个指针:一个指针 p
用于遍历单链表,另一个指针 pre
指向 p
的前驱结点。同时,我们还需要两个指针 minp
和 minpre
来分别保存当前找到的最小值结点和它的前驱结点。算法步骤如下:
初始化 p
指向头结点的下一个结点,pre
指向头结点。
将 minp
和 minpre
初始化为 p
和 pre
,以保存当前的最小值结点及其前驱。
遍历整个链表:
如果当前结点的值小于 minp
的值,则更新 minp
和 minpre
。
否则,移动 pre
和 p
指针到下一个结点。
遍历结束后,minp
指向链表中唯一的最小值结点,minpre
指向它的前驱结点。
删除 minp
所指的结点,并释放其空间。
代码实现: 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;LinkList DeleteMin (LinkList &L) { LNode *pre = L; LNode *p = pre->next; LNode *minpre = pre; LNode *minp = p; while (p != NULL ) { if (p->data < minp->data) { minp = p; minpre = pre; } pre = p; p = p->next; } if (minp != NULL ) { minpre->next = minp->next; delete minp; } return L; } void PrintList (LinkList L) { LNode *p = L->next; while (p != NULL ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { LinkList head = new LNode{0 , nullptr }; head->next = new LNode{3 , nullptr }; head->next->next = new LNode{1 , nullptr }; head->next->next->next = new LNode{4 , nullptr }; head->next->next->next->next = new LNode{2 , nullptr }; cout << "原链表:" ; PrintList (head); DeleteMin (head); cout << "删除最小值结点后的链表:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
时间复杂度和空间复杂度
时间复杂度 :该算法需要从头到尾遍历链表,因此时间复杂度为 $ O(n) $,其中 $ n $ 是链表中结点的数量。
空间复杂度 :由于只使用了有限的指针,空间复杂度为 $ O(1) $。
问题05. 试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间复杂度为 $ O(1) $。 解答 要实现带头结点的单链表的就地逆置,可以采用两种方法:头插法和指针反转法。以下是对这两种方法的详细解释和实现代码。
解法一:头插法 通过将每个结点依次摘下,并插入到头结点之后,实现链表的逆置。具体步骤如下:
初始化指针 p
指向第一个结点,r
用于存储 p
的后继结点。
将头结点的 next
指针置为 NULL
,表示链表暂时为空。
在遍历链表的过程中:
将当前结点 p
的后继结点存储在 r
中。
将 p
插入到头结点之后。
更新 p
指针为 r
,继续遍历。
重复上述步骤,直到遍历完整个链表。
代码实现: 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;LinkList Reverse1 (LinkList &L) { LNode *p = L->next; L->next = NULL ; while (p != NULL ) { LNode *r = p->next; p->next = L->next; L->next = p; p = r; } return L; } void PrintList (LinkList L) { LNode *p = L->next; while (p != NULL ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { LinkList head = new LNode{0 , nullptr }; head->next = new LNode{1 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{3 , nullptr }; head->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head); Reverse1 (head); cout << "逆置后的链表:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
解法二:指针反转法 该方法通过维护三个指针 pre
、p
和 r
,实现指针的反转。具体步骤如下:
初始化指针 p
指向第一个结点,r
指向 p
的下一个结点,pre
为 NULL
。
将 p
的 next
指针置为 NULL
,以便将它作为新的尾结点。
在遍历链表的过程中:
将 r
设为 p
的下一个结点。
将 p
的 next
指针指向 pre
,实现指针反转。
更新 pre
为当前的 p
,将 p
更新为 r
,继续遍历。
最后将头结点的 next
指针指向最后一个结点 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 LinkList Reverse2 (LinkList &L) { LNode *pre = NULL ; LNode *p = L->next; LNode *r = p != NULL ? p->next : NULL ; while (p != NULL ) { p->next = pre; pre = p; p = r; r = (r != NULL ) ? r->next : NULL ; } L->next = pre; return L; } int main () { LinkList head = new LNode{0 , nullptr }; head->next = new LNode{1 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{3 , nullptr }; head->next->next->next->next = new LNode{4 , nullptr }; cout << "原链表:" ; PrintList (head); Reverse2 (head); cout << "逆置后的链表:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
时间复杂度和空间复杂度
时间复杂度 :两个算法都需要遍历链表一次,因此时间复杂度为 $ O(n) $,其中 $ n $ 是链表中结点的数量。
空间复杂度 :这两个算法都只使用有限的指针,不需要额外的空间,因此空间复杂度为 $ O(1) $。
问题06. 有一个带头结点的单链表,设计一个算法使其元素递增有序。 解答 要将带头结点的单链表中的元素排序,可以采用直接插入排序的思想。算法的基本思路是:
构造初始有序链表 :首先,创建一个只包含一个结点的有序链表(即链表的第一个实际元素)。
扫描剩余结点 :然后依次扫描链表中剩下的结点 $ p $,并在已排序的链表中找到合适的位置(前驱结点 $ pre $),将当前结点 $ p $ 插入到 $ pre $ 之后。
代码实现 以下是实现上述思路的代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void sort (LinkList &L) { LNode *p = L->next; LNode *pre; LNode *r = p->next; p->next = NULL ; while (p != NULL ) { r = p->next; pre = L; while (pre->next != NULL && pre->next->data < p->data) { pre = pre->next; } p->next = pre->next; pre->next = p; p = r; } } void PrintList (LinkList L) { LNode *p = L->next; while (p != NULL ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { LinkList head = new LNode{0 , nullptr }; head->next = new LNode{4 , nullptr }; head->next->next = new LNode{2 , nullptr }; head->next->next->next = new LNode{1 , nullptr }; head->next->next->next->next = new LNode{3 , nullptr }; cout << "原链表:" ; PrintList (head); sort (head); cout << "排序后的链表:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->next; delete temp; } return 0 ; }
代码解释
初始化 :
LNode *p = L->next;
:指向链表中的第一个实际数据结点。
LNode *pre;
:用于查找插入位置的前驱结点。
LNode *r = p->next;
:保存当前结点的后继结点。
构建初始有序链表 :
将第一个结点的 next
指针置为 NULL
,表示构造了一个只含有一个结点的有序链表。
主循环 :
遍历链表中的每个结点,直到处理完所有结点:
使用 while
循环查找插入位置,找到 p
应该插入的位置的前驱结点 pre
。
将当前结点 p
插入到 pre
的后面。
更新 p
为下一个结点 r
。
时间复杂度和空间复杂度
时间复杂度 :该算法的时间复杂度为 $ O(n^2) $,因为在最坏情况下,每个结点都需要遍历已有的有序链表进行插入。
空间复杂度 :空间复杂度为 $ O(1) $,因为只使用了常数个指针。
改进方案 为了提高排序效率,可以考虑先将链表中的数据复制到数组中,使用更高效的排序算法(如归并排序、快速排序等)进行排序,然后再将排序后的元素依次插入到链表中。这样可以将时间复杂度降低到 $ O(n \log n) $,但空间复杂度会增加到 $ O(n) $,因为需要额外的数组来存储数据。
问题07. 设在一个带表头结点的单链表中所有元素结点的数据值无序,试编写一个函数,删除表中所有介于给定的两个值(作为函数参数给出)之间的元素(若存在)。 解答 由于链表是无序的,我们必须逐个遍历结点并检查每个结点的数据值。如果数据值介于给定的两个值之间,则需要删除该结点。下面是实现该功能的代码。
代码实现 以下是删除链表中介于给定两个值之间的元素的函数:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <iostream> using namespace std;struct LNode { int data; LNode *link; }; typedef LNode* LinkList;void RangeDelete (LinkList &L, int min, int max) { LNode *pr = L; LNode *p = L->link; while (p != NULL ) { if (p->data > min && p->data < max) { pr->link = p->link; delete p; p = pr->link; } else { pr = p; p = p->link; } } } void PrintList (LinkList L) { LNode *p = L->link; while (p != NULL ) { cout << p->data << " " ; p = p->link; } cout << endl; } int main () { LinkList head = new LNode{0 , nullptr }; head->link = new LNode{5 , nullptr }; head->link->link = new LNode{3 , nullptr }; head->link->link->link = new LNode{8 , nullptr }; head->link->link->link->link = new LNode{2 , nullptr }; head->link->link->link->link->link = new LNode{6 , nullptr }; cout << "原链表:" ; PrintList (head); RangeDelete (head, 2 , 6 ); cout << "删除后的链表:" ; PrintList (head); while (head != NULL ) { LNode *temp = head; head = head->link; delete temp; } return 0 ; }
代码解释
初始化 :
LNode *pr = L;
:前驱指针初始化为头结点。
LNode *p = L->link;
:当前结点指针初始化为第一个数据结点。
主循环 :
使用 while (p != NULL)
遍历整个链表:
如果当前结点的值在给定范围(min
和 max
)之间,则执行删除操作。
pr->link = p->link;
:将前驱结点的 link
指向当前结点的下一个结点,从而实现删除。
delete p;
:释放当前结点的内存。
更新 p
为下一个结点:p = pr->link;
。
如果当前结点的值不在范围内,则更新前驱指针和当前结点指针:
时间复杂度和空间复杂度
时间复杂度 :该算法的时间复杂度为 $ O(n) $,因为我们需要遍历整个链表。
空间复杂度 :空间复杂度为 $ O(1) $,因为只使用了常数个指针。
示例输出 运行上述代码的结果将是:
1 2 原链表:5 3 8 2 6 删除后的链表:5 8
这说明原链表中的值 3
和 2
被成功删除。
问题08. 给定两个单链表,编写算法找出两个链表的公共结点。 解答 两个单链表如果有公共结点,那么从某一结点开始,它们的 next
都指向同一个结点。为了找到这个公共结点,我们可以采用线性时间复杂度的算法。具体思路如下:
计算两个链表的长度 ,并确定哪个链表更长。
在较长的链表上先遍历长度差的结点 ,这样两个链表的指针在相同的位置开始同步遍历。
同时遍历两个链表 ,如果找到相同的结点,则返回该结点;如果遍历结束仍未找到,返回 NULL
。
代码实现 以下是实现上述思路的代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;int Length (LinkList L) { int length = 0 ; LNode* p = L->next; while (p != NULL ) { length++; p = p->next; } return length; } LinkList SearchFirstCommon (LinkList L1, LinkList L2) { int len1 = Length (L1); int len2 = Length (L2); LinkList longList, shortList; int dist; if (len1 > len2) { longList = L1->next; shortList = L2->next; dist = len1 - len2; } else { longList = L2->next; shortList = L1->next; dist = len2 - len1; } while (dist--) { longList = longList->next; } while (longList != NULL && shortList != NULL ) { if (longList == shortList) { return longList; } else { longList = longList->next; shortList = shortList->next; } } return NULL ; } void PrintList (LinkList L) { LNode *p = L->next; while (p != NULL ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { LinkList L1 = new LNode{0 , nullptr }; L1->next = new LNode{1 , nullptr }; L1->next->next = new LNode{2 , nullptr }; LinkList L2 = new LNode{0 , nullptr }; L2->next = new LNode{3 , nullptr }; L2->next->next = L1->next->next; cout << "链表 L1: " ; PrintList (L1); cout << "链表 L2: " ; PrintList (L2); LinkList commonNode = SearchFirstCommon (L1, L2); if (commonNode) { cout << "第一个公共结点: " << commonNode->data << endl; } else { cout << "没有公共结点" << endl; } return 0 ; }
代码解释
长度计算 :
Length(LinkList L)
:计算链表的长度。
寻找公共结点 :
使用 SearchFirstCommon(LinkList L1, LinkList L2)
函数找到两个链表的第一个公共结点。
首先计算两个链表的长度,并确定哪个更长。
在长链表上先移动长度差的结点,使得两个链表在同一位置开始遍历。
然后同时遍历两个链表,若找到相同的结点,则返回该结点;若遍历结束仍未找到,则返回 NULL
。
时间复杂度和空间复杂度
时间复杂度 :该算法的时间复杂度为 $ O(len1 + len2) $,因为需要遍历两个链表。
空间复杂度 :空间复杂度为 $ O(1) $,只使用了常数个指针。
示例输出 运行上述代码的结果将是:
1 2 3 链表 L1: 1 2 链表 L2: 3 2 第一个公共结点: 2
这说明链表 L1 和 L2 的第一个公共结点的值为 2
。
问题09. 给定一个带表头结点的单链表,设 head
为头指针,结点结构为 (data, next)
,data
为整型元素,next
为指针。试写出算法: 按递增次序输出单链表中各结点的数据元素并释放结点所占的存储空间 (要求: 不允许使用数组作为辅助空间)。 解答 算法思想 : 通过遍历链表,逐步找出链表中的最小值,输出该值并释放其占用的存储空间。重复这个过程,直到链表为空。最后释放头结点所占的空间。
时间复杂度 :该算法的时间复杂度为 $O(n^2)$,其中 $n$ 是链表的结点数。这是因为每次查找最小值都需要遍历剩余的结点。
代码实现 以下是实现上述思路的代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void MinDelete (LinkList &head) { while (head->next != NULL ) { LNode *pre = head; LNode *p = pre->next; while (p != NULL ) { if (p->data < pre->next->data) { pre = p; } p = p->next; } cout << pre->next->data << " " ; LNode *u = pre->next; pre->next = u->next; free (u); } free (head); } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } int main () { int arr[] = {3 , 1 , 4 , 2 }; LinkList head = CreateList (arr, 4 ); cout << "链表中的元素按递增顺序输出: " ; MinDelete (head); cout << endl; return 0 ; }
代码解释
结点结构 :
struct LNode
定义了链表结点的数据结构。
MinDelete 函数 :
MinDelete(LinkList &head)
:该函数通过循环查找链表中的最小值,输出并释放该结点的存储空间。
在外层 while
循环中,继续遍历链表,直到只剩下头结点。
在内层 while
循环中,查找当前链表中的最小值,并更新 pre
指针指向该最小值的前驱结点。
找到最小值后,输出该值,调整指针,释放结点空间。
辅助函数 :
CreateList(int arr[], int n)
:该函数用于根据数组创建链表。
主函数 :
main()
中创建了一个示例链表,并调用 MinDelete
函数输出链表中的元素。
示例输出 运行上述代码的结果将是:
这说明链表中的元素按递增顺序成功输出,并释放了所有结点的存储空间。
问题10. 将一个带头结点的单链表 $A$ 分解为两个带头结点的单链表 $A’$ 和 $B$,使得 $A’$ 表中含有原表中序号为奇数的元素,而 $B$ 表中含有原表中序号为偶数的元素,且保持其相对顺序不变。 解答 算法思想 : 使用一个访问序号变量(初始值为0),在遍历链表的每个结点时,序号自动加1。根据序号的奇偶性将结点插入到 $A’$ 表或 $B$ 表中。重复此操作直到遍历完链表。
时间复杂度 :该算法的时间复杂度为 $O(n)$,其中 $n$ 是链表的结点数。
代码实现 以下是实现上述思路的代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void DisCreate (LinkList &A, LinkList &B) { int i = 0 ; B = new LNode{0 , nullptr }; LNode *ra = A; LNode *rb = B; LNode *p = A->next; while (p != nullptr ) { i++; if (i % 2 == 1 ) { ra->next = p; ra = p; } else { rb->next = p; rb = p; } p = p->next; } ra->next = nullptr ; rb->next = nullptr ; } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arr[] = {1 , 2 , 3 , 4 , 5 }; LinkList A = CreateList (arr, 5 ); LinkList B; DisCreate (A, B); cout << "链表 A' 中的元素: " ; PrintList (A); cout << "链表 B 中的元素: " ; PrintList (B); delete A; delete B; return 0 ; }
代码解释
结点结构 :
struct LNode
定义了链表结点的数据结构。
DisCreate 函数 :
DisCreate(LinkList &A, LinkList &B)
:该函数根据奇偶性将链表 $A$ 分解为链表 $A’$ 和 $B$。
使用 i
记录当前结点的序号。每访问一个结点,序号加1。
根据序号的奇偶性,将结点插入到相应的链表 $A’$ 或 $B$ 中。
最后,分别将 $A’$ 和 $B$ 的尾结点的 next
指针设为 nullptr
。
辅助函数 :
CreateList(int arr[], int n)
:该函数用于根据数组创建链表。
PrintList(LinkList head)
:该函数用于打印链表的元素。
主函数 :
main()
中创建了一个示例链表 $A$,调用 DisCreate
函数将其分解为链表 $A’$ 和 $B$,然后打印两个链表的内容。
示例输出 运行上述代码的结果将是:
1 2 链表 A' 中的元素: 1 3 5 链表 B 中的元素: 2 4
这表明链表 $A$ 成功分解为两个链表,$A’$ 中包含原链表中序号为奇数的元素,$B$ 中包含序号为偶数的元素,并保持了相对顺序不变。
问题11. 设 $ C=\{a_1, b_1, a_2, b_2, \ldots, a_n, b_n\} $ 为线性表,采用带头结点的单链表存放,设计一个就地算法,将其拆分为两个线性表,使得 $ A=\{a_1, a_2, \ldots, a_n\} $,$ B=\{b_1, b_2, \ldots, b_n\} $。 解答 算法思想 : 本算法采用与前一题相似的思路,不设序号变量。由于 $ B $ 表的建立采用头插法,因此每次将新结点插入到 $ B $ 的表头。这种方法可以在遍历时直接修改原链表的结构,同时保持 $ A $ 和 $ B $ 的顺序。
时间复杂度 :该算法的时间复杂度为 $ O(n) $,其中 $ n $ 是链表的结点数。
代码实现 以下是实现上述思路的代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <iostream> using namespace std;struct LNode { char data; LNode *next; }; typedef LNode* LinkList;LinkList DisCreate2 (LinkList &C) { LinkList B = new LNode{0 , nullptr }; LNode *ra = C; LNode *p = C->next; while (p != nullptr ) { ra->next = p; ra = p; p = p->next; if (p != nullptr ) { LNode *q = p->next; p->next = B->next; B->next = p; p = q; } } ra->next = nullptr ; return B; } LinkList CreateList (char arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { char arr[] = {'a' , 'b' , 'c' , 'd' , 'e' , 'f' }; LinkList C = CreateList (arr, 6 ); LinkList B; B = DisCreate2 (C); cout << "链表 A 中的元素: " ; PrintList (C); cout << "链表 B 中的元素: " ; PrintList (B); delete C; delete B; return 0 ; }
代码解释
结点结构 :
struct LNode
定义了链表结点的数据结构,包含数据域和指针域。
DisCreate2 函数 :
DisCreate2(LinkList &C)
:该函数根据链表 $C$ 拆分出链表 $A$ 和链表 $B$。
LinkList B = new LNode{0, nullptr};
:创建链表 $B$ 的头结点。
LNode *ra = C;
:ra
指向 $A$ 的尾结点。
LNode *p = C->next;
:p
为工作指针,指向待处理的结点。
在 while
循环中:
将当前结点链接到 $A$ 的尾部,并更新尾指针 ra
。
如果 p
的下一个结点存在,则将当前结点链接到 $B$ 的头部。
使用 q
保存下一个待处理的结点,以避免链表断链。
最后,终止 $A$ 的链表并返回 $B$ 的头结点。
辅助函数 :
CreateList(char arr[], int n)
:根据字符数组创建链表。
PrintList(LinkList head)
:打印链表的元素。
主函数 :
main()
中创建了一个示例链表 $C$,调用 DisCreate2
拆分为链表 $A$ 和 $B$,然后打印两个链表的内容。
示例输出 运行上述代码的结果将是:
1 2 链表 A 中的元素: a c e 链表 B 中的元素: f d b
这表明链表 $C$ 成功拆分为两个链表,$A$ 中包含原链表中 $a$ 的元素,$B$ 中包含 $b$ 的元素,且保持了相对顺序不变。
问题12:在一个递增有序的线性表中,存在数值相同的元素。若存储方式为单链表,设计一个算法去掉数值相同的元素,使表中不再有重复的元素。例如,输入为 (7, 10, 10, 21, 30, 42, 42, 42, 51, 70)
,将变为 (7, 10, 21, 30, 42, 51, 70)
。 解答 算法思想 由于链表是递增有序的,因此所有相同值域的结点都是相邻的。我们可以使用一个指针 $p$ 来扫描链表,如果当前结点的值与后继结点的值相同,则删除后继结点,否则移动指针 $p$ 到下一个结点。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void DelSame (LinkList &l) { LNode *p = l->next; LNode *q; while (p != nullptr && p->next != nullptr ) { q = p->next; if (p->data == q->data) { p->next = q->next; delete q; } else { p = p->next; } } } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arr[] = {7 , 10 , 10 , 21 , 30 , 42 , 42 , 42 , 51 , 70 }; LinkList l = CreateList (arr, 10 ); cout << "原始链表: " ; PrintList (l); DelSame (l); cout << "去重后的链表: " ; PrintList (l); LNode *p = l; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } return 0 ; }
代码说明
时间与空间复杂度
时间复杂度 :$O(n)$,因为每个结点仅被访问一次。
空间复杂度 :$O(1)$,不使用额外的存储空间。
示例输出 运行该代码,输出将为:
1 2 原始链表: 7 10 10 21 30 42 42 42 51 70 去重后的链表: 7 10 21 30 42 51 70
问题13:假设有两个按元素值递增次序排列的线性表,均以单链表形式存储。请编写算法将这两个单链表归并为一个按元素值递减次序排列的单链表,并要求利用原来两个单链表的结点存放归并后的单链表。 解答 算法思想 由于两个链表已经按元素值递增排序,我们可以同时从两个链表的头结点开始进行比较。每次将较小的结点插入到新链表的头部,以达到按元素值递减的顺序。比较结束后,可能会有一个链表非空,此时将剩下的结点依次插入新链表中。归并后的链表采用头插法实现。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void MergeList (LinkList &La, LinkList &Lb) { LNode *r, *pa = La->next, *pb = Lb->next; La->next = nullptr ; while (pa && pb) { if (pa->data <= pb->data) { r = pa->next; pa->next = La->next; La->next = pa; pa = r; } else { r = pb->next; pb->next = La->next; La->next = pb; pb = r; } } while (pa) { r = pa->next; pa->next = La->next; La->next = pa; pa = r; } while (pb) { r = pb->next; pb->next = La->next; La->next = pb; pb = r; } delete Lb; Lb = nullptr ; } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arr1[] = {1 , 3 , 5 , 7 }; int arr2[] = {2 , 4 , 6 , 8 }; LinkList La = CreateList (arr1, 4 ); LinkList Lb = CreateList (arr2, 4 ); cout << "第一个链表: " ; PrintList (La); cout << "第二个链表: " ; PrintList (Lb); MergeList (La, Lb); cout << "合并后的链表: " ; PrintList (La); LNode *p = La; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } return 0 ; }
代码说明
时间与空间复杂度
时间复杂度 :$O(m+n)$,其中 $m$ 和 $n$ 分别为两个链表的结点数。
空间复杂度 :$O(1)$,只使用了常量级的额外空间。
示例输出 运行该代码,输出将为:
1 2 3 第一个链表: 1 3 5 7 第二个链表: 2 4 6 8 合并后的链表: 8 7 6 5 4 3 2 1
问题14:设 A 和 B 是两个单链表(带头结点),其中元素递增有序。设计一个算法从 A 和 B 中的公共元素产生单链表 C,要求不破坏 A 和 B 的结点。 解答 算法思想 由于链表 A 和 B 都是有序的,我们可以使用两个指针分别遍历这两个链表,依次比较它们的元素值。如果元素值相等,就创建一个新结点并将其插入到链表 C 中。如果元素值不相等,则将指向较小元素的指针后移,继续比较。该过程持续进行,直到遍历到其中一个链表的尾部。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void GetCommon (LinkList A, LinkList B, LinkList &C) { LNode *p = A->next; LNode *q = B->next; LNode *r = C; while (p != nullptr && q != nullptr ) { if (p->data < q->data) { p = p->next; } else if (p->data > q->data) { q = q->next; } else { LNode *s = new LNode; s->data = p->data; r->next = s; r = s; p = p->next; q = q->next; } } r->next = nullptr ; } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arrA[] = {1 , 2 , 3 , 4 , 5 }; int arrB[] = {3 , 4 , 5 , 6 , 7 }; LinkList A = CreateList (arrA, 5 ); LinkList B = CreateList (arrB, 5 ); LinkList C = new LNode{0 , nullptr }; cout << "链表 A: " ; PrintList (A); cout << "链表 B: " ; PrintList (B); GetCommon (A, B, C); cout << "公共元素链表 C: " ; PrintList (C); LNode *p = A; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } p = B; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } p = C; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } return 0 ; }
代码说明
时间与空间复杂度
时间复杂度 :$O(m+n)$,其中 $m$ 和 $n$ 分别为两个链表的结点数。
空间复杂度 :$O(1)$,只使用了常量级的额外空间,除了新创建的公共元素结点外。
示例输出 运行该代码,输出将为:
1 2 3 链表 A: 1 2 3 4 5 链表 B: 3 4 5 6 7 公共元素链表 C: 3 4 5
问题15:已知两个链表 $ A $ 和 $ B $ 分别表示两个集合,其元素递增排列。编制函数,求 $ A $ 与 $ B $ 的交集,并存放于 $ A $ 链表中。 解答 算法思想 采用归并的思想,设置两个工作指针 $ pa $ 和 $ pb $,对两个链表进行归并扫描。只有同时出现在两个集合中的元素才链接到结果表中且仅保留一个,其他的结点全部释放。当一个链表遍历完毕后,释放另一个表中剩下的全部结点。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;LinkList Union (LinkList &A, LinkList &B) { LNode *pa = A->next; LNode *pb = B->next; LNode *pc = A; while (pa && pb) { if (pa->data == pb->data) { pc->next = pa; pc = pa; pa = pa->next; LNode *u = pb; pb = pb->next; delete u; } else if (pa->data < pb->data) { LNode *u = pa; pa = pa->next; delete u; } else { LNode *u = pb; pb = pb->next; delete u; } } while (pa) { LNode *u = pa; pa = pa->next; delete u; } while (pb) { LNode *u = pb; pb = pb->next; delete u; } pc->next = nullptr ; delete B; return A; } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arrA[] = {1 , 2 , 3 , 4 , 5 }; int arrB[] = {3 , 4 , 5 , 6 , 7 }; LinkList A = CreateList (arrA, 5 ); LinkList B = CreateList (arrB, 5 ); cout << "链表 A: " ; PrintList (A); cout << "链表 B: " ; PrintList (B); Union (A, B); cout << "交集链表 A: " ; PrintList (A); LNode *p = A; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } return 0 ; }
代码说明
时间与空间复杂度
时间复杂度 :$ O(lenA + lenB) $,其中 $ lenA $ 和 $ lenB $ 分别为两个链表的结点数。
空间复杂度 :$ O(1) $,只使用了常量级的额外空间,除了新创建的公共元素结点外。
示例输出 运行该代码,输出将为:
1 2 3 链表 A: 1 2 3 4 5 链表 B: 3 4 5 6 7 交集链表 A: 3 4 5
问题16:已知两个整数序列 $ A = a_1, a_2, a_3, \ldots, a_m $ 和 $ B = b_1, b_2, b_3, \ldots, b_n $,已经存入两个单链表中。设计一个算法,判断序列 $ B $ 是否是序列 $ A $ 的连续子序列。 解答 算法思想 从两个链表的第一个结点开始进行比较。若对应的数据相等,则后移指针;若不等,则链表 $ A $ 的指针从上次比较结点的后继开始,链表 $ B $ 的指针仍从第一个结点开始比较。若链表 $ B $ 到达尾部而 $ A $ 还未到尾,则表示匹配成功;否则,若 $ A $ 到达尾部而 $ B $ 未到尾,则表示匹配失败。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;int Pattern (LinkList A, LinkList B) { LNode *p = A; LNode *q = B; while (p) { LNode *pre = p; q = B; while (p && q && p->data == q->data) { p = p->next; q = q->next; } if (q == nullptr ) { return 1 ; } p = pre->next; } return 0 ; } LinkList CreateList (int arr[], int n) { LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } return head; } void PrintList (LinkList head) { LNode *p = head->next; while (p != nullptr ) { cout << p->data << " " ; p = p->next; } cout << endl; } int main () { int arrA[] = {1 , 2 , 3 , 4 , 5 }; int arrB[] = {3 , 4 }; LinkList A = CreateList (arrA, 5 ); LinkList B = CreateList (arrB, 2 ); cout << "链表 A: " ; PrintList (A); cout << "链表 B: " ; PrintList (B); if (Pattern (A, B)) { cout << "B 是 A 的连续子序列" << endl; } else { cout << "B 不是 A 的连续子序列" << endl; } LNode *p = A; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } p = B; while (p != nullptr ) { LNode *temp = p; p = p->next; delete temp; } return 0 ; }
代码说明
时间与空间复杂度
示例输出 运行该代码,输出将为:
1 2 3 链表 A: 1 2 3 4 5 链表 B: 3 4 B 是 A 的连续子序列
问题17:有两个循环单链表,链表头指针分别为 $ h_1 $ 和 $ h_2 $。编写一个函数将链表 $ h_2 $ 链接到链表 $ h_1 $ 之后,要求链接后的链表仍保持循环链表的形式。 解答 算法思想
首先,找到链表 $ h_1 $ 的尾结点和链表 $ h_2 $ 的尾结点。
将链表 $ h_1 $ 的尾结点的 next
指针指向链表 $ h_2 $ 的头结点。
然后,将链表 $ h_2 $ 的尾结点的 next
指针指向链表 $ h_1 $ 的头结点,形成循环结构。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;LinkList Link (LinkList &h1, LinkList &h2) { if (h1 == nullptr ) return h2; if (h2 == nullptr ) return h1; LNode *p = h1; while (p->next != h1) { p = p->next; } LNode *q = h2; while (q->next != h2) { q = q->next; } p->next = h2; q->next = h1; return h1; } LinkList CreateCircularList (int arr[], int n) { if (n == 0 ) return nullptr ; LinkList head = new LNode{arr[0 ], nullptr }; LNode *tail = head; for (int i = 1 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } tail->next = head; return head; } void PrintCircularList (LinkList head) { if (head == nullptr ) return ; LNode *p = head; do { cout << p->data << " " ; p = p->next; } while (p != head); cout << endl; } int main () { int arr1[] = {1 , 2 , 3 }; int arr2[] = {4 , 5 }; LinkList h1 = CreateCircularList (arr1, 3 ); LinkList h2 = CreateCircularList (arr2, 2 ); cout << "链表 h1: " ; PrintCircularList (h1); cout << "链表 h2: " ; PrintCircularList (h2); Link (h1, h2); cout << "链接后的链表 h1: " ; PrintCircularList (h1); LNode *p = h1; if (p != nullptr ) { LNode *temp; do { temp = p; p = p->next; delete temp; } while (p != h1); } return 0 ; }
代码说明
时间与空间复杂度
示例输出 运行该代码,输出将为:
1 2 3 链表 h1: 1 2 3 链表 h2: 4 5 链接后的链表 h1: 1 2 3 4 5
问题18:设有一个带头结点的循环单链表,其节点值均为正整数。设计一个算法,反复找出单链表中节点值最小的节点并输出,然后将该节点从中删除,直到单链表为空为止,最后删除表头节点。 解答 算法思想
使用两个指针 p
和 pre
遍历循环单链表,p
用于指向当前节点,pre
用于指向 p
的前驱节点。
在每次循环中,找出最小值节点 minp
和它的前驱节点 minpre
。
找到最小节点后,将其从链表中删除,并输出该节点的值。
重复上述步骤,直到链表为空。
最后释放头结点。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> using namespace std;struct LNode { int data; LNode *next; }; typedef LNode* LinkList;void DelAll (LinkList &L) { LNode *p, *pre, *minp, *minpre; while (L->next != L) { p = L->next; pre = L; minp = p; minpre = pre; while (p != L) { if (p->data < minp->data) { minp = p; minpre = pre; } pre = p; p = p->next; } cout << "最小值结点: " << minp->data << endl; minpre->next = minp->next; free (minp); } free (L); } LinkList CreateCircularList (int arr[], int n) { if (n == 0 ) return nullptr ; LinkList head = new LNode{0 , nullptr }; LNode *tail = head; for (int i = 0 ; i < n; ++i) { LNode *newNode = new LNode{arr[i], nullptr }; tail->next = newNode; tail = newNode; } tail->next = head; return head; } int main () { int arr[] = {3 , 1 , 4 , 1 , 5 , 9 , 2 }; LinkList L = CreateCircularList (arr, 7 ); cout << "删除循环链表中的所有最小值结点:" << endl; DelAll (L); return 0 ; }
代码说明
DelAll
函数 :该函数实现每次删除循环单链表中的最小元素,直到链表为空。使用指针 minp
和 minpre
找到最小值节点及其前驱节点,并在找到后将其从链表中删除。
辅助函数 :CreateCircularList
用于创建循环单链表。
时间与空间复杂度
时间复杂度 :$ O(n^2) $,每次删除最小节点时需要遍历链表一次,重复此操作直到链表为空。
空间复杂度 :$ O(1) $,只使用了常量级的额外空间。
示例输出 运行该代码,输出将为:
1 2 3 4 5 6 7 8 删除循环链表中的所有最小值结点: 最小值结点: 1 最小值结点: 1 最小值结点: 2 最小值结点: 3 最小值结点: 4 最小值结点: 5 最小值结点: 9
问题19: 设头指针为 L
的带有表头结点的非循环双向链表,其每个节点中除了有 pred
(前驱指针)、data
(数据)和 next
(后继指针)域外,还有一个访问频度域 freq
。在链表被启用前,freq
值均初始化为零。
每当在链表中进行一次 Locate(L, x)
运算时:
令元素值为 x
的节点中 freq
域的值增 1。
使此链表中的节点保持按访问频度非增(递减)的顺序排列,同时最近访问的节点排在频度相同的节点前面。
请编写符合上述要求的 Locate(L, x)
运算的算法,该运算为函数过程,返回找到节点的地址,类型为指针型。
解答 算法思想
首先,在双向链表中查找数据值为 x
的节点。
如果找到该节点,将其摘下(从链表中删除)。
增加该节点的 freq
值。
在链表中查找该节点的新插入位置,保持按频度递减排序,且同频度节点按最近访问顺序排列。
将节点插入到找到的位置。
代码实现 以下是 C++ 代码示例,用于实现上述算法:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #include <iostream> using namespace std;struct DNode { int data; DNode *pred; DNode *next; int freq; }; typedef DNode* DLinkList; DLinkList Locate (DLinkList &L, int x) { DNode *p = L->next; DNode *q; while (p != nullptr && p->data != x) { p = p->next; } if (p == nullptr ) { cout << "节点值为 " << x << " 的结点不存在。" << endl; exit (0 ); } p->freq++; if (p->pred == L || p->pred->freq > p->freq) { return p; } if (p->next != nullptr ) { p->next->pred = p->pred; } p->pred->next = p->next; q = p->pred; while (q != L && q->freq <= p->freq) { q = q->pred; } p->next = q->next; if (q->next != nullptr ) { q->next->pred = p; } p->pred = q; q->next = p; return p; } DLinkList CreateDoublyLinkedList (int arr[], int n) { if (n == 0 ) return nullptr ; DLinkList head = new DNode{0 , nullptr , nullptr , 0 }; DNode *tail = head; for (int i = 0 ; i < n; ++i) { DNode *newNode = new DNode{arr[i], tail, nullptr , 0 }; tail->next = newNode; tail = newNode; } tail->next = nullptr ; return head; } int main () { int arr[] = {5 , 3 , 8 , 1 , 7 }; DLinkList L = CreateDoublyLinkedList (arr, 5 ); int x = 3 ; DNode *result = Locate (L, x); cout << "找到节点值 " << result->data << ",频度为 " << result->freq << endl; DNode *temp = L->next; cout << "链表节点值及频度: " << endl; while (temp != nullptr ) { cout << "节点值: " << temp->data << ", 频度: " << temp->freq << endl; temp = temp->next; } return 0 ; }
代码说明
Locate
函数 :该函数在双向链表中查找值为 x
的节点,并在频度更新后重新插入到正确位置。
辅助函数 :CreateDoublyLinkedList
用于创建一个带有头结点的双向链表。
时间与空间复杂度
时间复杂度 :最坏情况下为 $O(n)$,因为需要遍历整个链表。
空间复杂度 :$O(1)$,只使用了常量级的额外空间。
示例输出 运行该代码,输出将为:
1 2 3 4 5 6 7 找到节点值 3,频度为 1 链表节点值及频度: 节点值: 5, 频度: 0 节点值: 3, 频度: 1 节点值: 8, 频度: 0 节点值: 1, 频度: 0 节点值: 7, 频度: 0
问题20:单链表有环 ,是指单链表的最后一个结点的指针指向了链表中的某个结点(通常单链表的最后一个结点的指针域是空的)。本题要求编写算法判断单链表是否存在环。 1) 基本设计思想 要判断单链表是否存在环,常用的算法是 快慢指针法 (也称为 Floyd 判圈算法 )。基本思想是使用两个指针,slow
和 fast
,从链表的头结点开始遍历:
slow
每次移动一步。
fast
每次移动两步。
如果链表中存在环,slow
和 fast
指针最终会相遇;如果链表没有环,fast
指针将会到达链表的末尾(即 nullptr
)。
2) C++ 代码实现 以下是用 C++ 实现判断单链表是否存在环的算法:
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 45 46 47 #include <iostream> struct Node { int data; Node* next; }; bool hasCycle (Node* head) { if (head == nullptr ) return false ; Node* slow = head; Node* fast = head; while (fast != nullptr && fast->next != nullptr ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { return true ; } } return false ; } int main () { Node* head = new Node{1 , nullptr }; head->next = new Node{2 , nullptr }; head->next->next = new Node{3 , nullptr }; head->next->next->next = new Node{4 , nullptr }; head->next->next->next->next = head->next; if (hasCycle (head)) { std::cout << "链表中存在环。" << std::endl; } else { std::cout << "链表中不存在环。" << std::endl; } return 0 ; }
3) 时间复杂度和空间复杂度
问题21:给定一个带有表头结点的单链表,要求查找链表中倒数第 $ k $ 个位置上的结点,并返回该结点的 data
域的值。若查找成功,输出该结点的 data
域的值并返回 1;若失败,返回 0。 1) 算法的基本设计思想 要查找链表中倒数第 $ k $ 个结点,我们可以使用 双指针法 。具体步骤如下:
定义两个指针 p
和 q
,初始都指向链表的第一个实际结点(头结点的下一个结点)。
移动指针 p
使其先走 $ k $ 步。
然后同时移动 p
和 q
,直到 p
到达链表的末尾。
当 p
到达末尾时,q
就会指向倒数第 $ k $ 个结点。
该方法只需遍历链表一次,因此时间复杂度为 $ O(n) $,空间复杂度为 $ O(1) $。
2) 算法的详细实现步骤
初始化 :将 count
设为 0,指针 p
和 q
都指向链表头结点的下一个结点。
检查空链表 :如果 p
为 NULL,返回 0(链表为空)。
移动指针 p
:
每次移动 p
指向下一个结点。
如果 count
小于 $ k $,则将 count
加 1。
否则,将 q
移动到下一个结点。
完成遍历 :当 p
到达链表末尾时,检查 count
是否小于 $ k $。
如果是,返回 0(链表长度小于 $ k $)。
否则,输出 q
的 data
值并返回 1。
3) C++ 实现 以下是用 C++ 实现该算法的代码:
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 45 46 47 48 49 50 51 52 53 54 55 #include <iostream> struct LNode { int data; LNode* link; }; typedef LNode* LinkList;int SearchK (LinkList list, int k) { LNode* p = list->link; LNode* q = list->link; int count = 0 ; while (p != nullptr ) { if (count < k) { count++; } else { q = q->link; } p = p->link; } if (count < k) { return 0 ; } else { std::cout << q->data << std::endl; return 1 ; } } int main () { LinkList list = new LNode{0 , nullptr }; list->link = new LNode{1 , nullptr }; list->link->link = new LNode{2 , nullptr }; list->link->link->link = new LNode{3 , nullptr }; list->link->link->link->link = new LNode{4 , nullptr }; int k = 2 ; int result = SearchK (list, k); if (result == 0 ) { std::cout << "链表中不存在倒数第 " << k << " 个节点。" << std::endl; } return 0 ; }
总结
时间复杂度 :$ O(n) $,遍历链表一次。
空间复杂度 :$ O(1) $,只使用了常量级的额外空间。
问题22:给定两个带头结点的单链表 str1
和 str2
,当两个单词有相同的后缀时,可以共享相同的后缀存储空间。设计一个算法找出两个链表共同后缀的起始位置。 1) 算法的基本设计思想
计算链表长度 :分别计算链表 str1
和 str2
的长度 $ m $ 和 $ n $。
调整指针 :令指针 p
和 q
分别指向 str1
和 str2
的头结点。
如果 $ m > n $,则让 p
先移动 $ m - n $ 步,使得 p
和 q
到达的后缀长度相等。
如果 $ m < n $,则让 q
先移动 $ n - m $ 步。
同步移动指针 :同时移动指针 p
和 q
,直到找到相同的位置,即为共同后缀的起始位置。
2) 算法的详细实现步骤
定义链表节点结构 :
1 2 3 4 typedef struct Node { char data; struct Node * next; } Node;
计算链表长度的函数 :
1 2 3 4 5 6 7 8 int listlen (Node* head) { int len = 0 ; while (head->next != NULL ) { len++; head = head->next; } return len; }
寻找共同后缀的起始地址的函数 :
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 Node* findAddr (Node* str1, Node* str2) { int m = listlen (str1); int n = listlen (str2); Node* p = str1->next; Node* q = str2->next; if (m > n) { for (int i = 0 ; i < (m - n); i++) { p = p->next; } } else { for (int i = 0 ; i < (n - m); i++) { q = q->next; } } while (p != NULL && q != NULL ) { if (p == q) { return p; } p = p->next; q = q->next; } return NULL ; }
3) 时间复杂度分析
时间复杂度 :$ O(m + n) $,其中 $ m $ 和 $ n $ 分别为链表 str1
和 str2
的长度。因为我们分别遍历了两个链表来计算长度,并再次遍历到找到共同后缀的起始位置。
空间复杂度 :$ O(1) $,只使用了常量级的额外空间。
问题23:设计一个算法,对于用单链表保存的 $ m $ 个整数,删除链表中绝对值相等的节点,仅保留第一次出现的节点。 1) 算法的基本设计思想 算法的核心思想是使用辅助数组来记录链表中已经出现的绝对值,利用空间换取时间,以便在一次遍历中完成对链表的处理:
辅助数组 :使用一个大小为 $ n + 1 $ 的辅助数组 q
(假设绝对值不超过 $ n $),数组初值为 0,用于标记每个整数的绝对值是否已出现。
遍历链表 :依次遍历链表中的每个节点,检查该节点的绝对值在数组 q
中的状态:
如果 q[m]
(当前节点的绝对值)为 0,表示该绝对值第一次出现,保留该节点,并将 q[m]
设置为 1。
如果 q[m]
不为 0,表示该绝对值已经出现过,删除该节点。
2) 单链表节点的数据类型定义(C语言) 1 2 3 4 5 6 typedef struct node { int data; struct node * link ; } NODE; typedef NODE* PNODE;
3) 算法实现 以下是实现该算法的 C 语言代码:
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 #include <stdio.h> #include <stdlib.h> typedef struct node { int data; struct node * link ; } NODE; typedef NODE* PNODE; void func (PNODE head, int n) { PNODE p = head, r; int *q; q = (int *)malloc (sizeof (int ) * (n + 1 )); for (int i = 0 ; i <= n; i++) { q[i] = 0 ; } while (p->link != NULL ) { int m = abs (p->link->data); if (q[m] == 0 ) { q[m] = 1 ; p = p->link; } else { r = p->link; p->link = r->link; free (r); } } free (q); }
4) 时间复杂度和空间复杂度分析
时间复杂度 :$ O(m) $,其中 $ m $ 是链表中的节点数量。我们只需遍历链表一次。
空间复杂度 :$ O(n) $,其中 $ n $ 是节点数据的绝对值的最大值。我们使用了一个大小为 $ n + 1 $ 的辅助数组。
问题24:设线性表 $ L = (a_1, a_2, \ldots, a_n) $ 采用带头结点的单链表保存,要求设计一个空间复杂度为 $ O(1) $ 且时间上尽可能高效的算法,将链表中的各节点重新排列为 $ L’ = (a_1, a_n, a_2, a_{n-1}, a_3, a_{n-2}, \ldots) $。 1) 算法的基本设计思想 根据给定的线性表 $ L $ 和目标线性表 $ L’ $,我们可以观察到 $ L’ $ 的形成是通过依次摘取前面的元素和后面的元素来实现的。为了高效地实现这一目标,采用以下步骤:
找中间节点 :使用快慢指针法找到链表的中间节点,慢指针每次走一步,快指针每次走两步。
逆置后半段链表 :将链表的后半部分原地逆置,方便后续交替取节点。
合并前后两段 :依次从前半段和逆置后的后半段中取节点,重新排列成所需的形式。
2) 算法实现(C语言) 以下是实现该算法的 C 语言代码:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <stdio.h> #include <stdlib.h> typedef struct node { int data; struct node * next ; } NODE; void change_list (NODE* head) { NODE *p, *q, *r, *s; p = head; q = head; while (q->next != NULL ) { p = p->next; q = q->next; if (q->next != NULL ) q = q->next; } q = p->next; p->next = NULL ; NODE* prev = NULL ; while (q != NULL ) { r = q->next; q->next = prev; prev = q; q = r; } p = head->next; s = head; q = prev; while (q != NULL ) { r = q->next; q->next = p; p = p->next; s->next = q; s = s->next; q = r; } } int main () { NODE* head = (NODE*)malloc (sizeof (NODE)); head->next = NULL ; change_list(head); return 0 ; }
3) 时间复杂度分析
时间复杂度 :整个算法的时间复杂度为 $ O(n) $,其中 $ n $ 是链表中的节点数量。我们分别进行了链表遍历(找中间节点)、逆置(重新排列)和合并(重新构造链表)的操作,均为线性复杂度。
空间复杂度 :算法的空间复杂度为 $ O(1) $,因为我们只使用了常量级的额外空间(指针变量)。
问题25:给定一个长度为 $ N $ 的整型数组 $ A[1..N] $ 和一个整数 $ X $,设计一个时间复杂度不超过 $ O(n \log n) $ 的算法,查找出这个数组中所有两两之和等于 $ X $ 的整数对(每个元素只输出一次)。 解决方案 方法一:排序 + 双指针
如提示中所述,可以先对数组进行排序,然后使用双指针法来查找满足条件的整数对。以下是具体步骤:
排序 :使用快速排序或归并排序等 $ O(n \log n) $ 的排序算法将数组 $ A $ 从小到大排序。
双指针查找 :设置两个指针,分别指向数组的开始和结束。根据两个指针指向的元素的和与 $ X $ 的关系来移动指针:
如果 $ A[i] + A[j] < X $,则移动左指针 $ i $(即增大 $ A[i] $)。
如果 $ A[i] + A[j] > X $,则移动右指针 $ j $(即减小 $ A[j] $)。
如果 $ A[i] + A[j] = X $,则输出 $ A[i] $ 和 $ A[j] $,然后同时移动两个指针。
算法实现 以下是实现该算法的 C 语言代码示例:
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 #include <stdio.h> #include <stdlib.h> int compare (const void * a, const void * b) { return (*(int *)a - *(int *)b); } void find_pairs (int * A, int N, int X) { qsort(A, N, sizeof (int ), compare); int i = 0 ; int j = N - 1 ; while (i < j) { int sum = A[i] + A[j]; if (sum < X) { i++; } else if (sum > X) { j--; } else { printf ("Pair found: (%d, %d)\n" , A[i], A[j]); i++; j--; } } } int main () { int A[] = {1 , 2 , 3 , 4 , 5 , 6 }; int N = sizeof (A) / sizeof (A[0 ]); int X = 7 ; find_pairs(A, N, X); return 0 ; }
时间复杂度分析
排序的时间复杂度 :使用 $ O(n \log n) $ 的排序算法(如快速排序)。
双指针查找的时间复杂度 :在最坏情况下,每个指针最多遍历一次整个数组,因此时间复杂度为 $ O(n) $。
整体时间复杂度 :算法的整体时间复杂度为 $ O(n \log n) $。
方法二:哈希表 除了上述方法外,还可以使用哈希表来解决该问题。具体步骤如下:
遍历数组,将所有元素存入哈希表中,记录出现的次数。
再次遍历数组,检查每个元素 $ a $ 是否存在 $ X - a $。
如果存在且没有输出过,则输出这一对。
更新哈希表以避免重复输出。
哈希表实现示例 以下是使用哈希表实现的 C 语言代码示例:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <stdio.h> #include <stdlib.h> typedef struct HashNode { int key; struct HashNode * next ; } HashNode; typedef struct HashTable { HashNode** table; int size; } HashTable; HashTable* create_table (int size) { HashTable* ht = (HashTable*)malloc (sizeof (HashTable)); ht->size = size; ht->table = (HashNode**)malloc (size * sizeof (HashNode*)); for (int i = 0 ; i < size; i++) { ht->table[i] = NULL ; } return ht; } int hash (int key, int size) { return key % size; } void insert (HashTable* ht, int key) { int index = hash(key, ht->size); HashNode* newNode = (HashNode*)malloc (sizeof (HashNode)); newNode->key = key; newNode->next = ht->table[index]; ht->table[index] = newNode; } int search (HashTable* ht, int key) { int index = hash(key, ht->size); HashNode* node = ht->table[index]; while (node != NULL ) { if (node->key == key) { return 1 ; } node = node->next; } return 0 ; } void free_table (HashTable* ht) { for (int i = 0 ; i < ht->size; i++) { HashNode* node = ht->table[i]; while (node != NULL ) { HashNode* temp = node; node = node->next; free (temp); } } free (ht->table); free (ht); } void find_pairs_with_hash (int * A, int N, int X) { HashTable* ht = create_table(N * 2 ); for (int i = 0 ; i < N; i++) { insert(ht, A[i]); } for (int i = 0 ; i < N; i++) { int complement = X - A[i]; if (search(ht, complement)) { printf ("Pair found: (%d, %d)\n" , A[i], complement); } } free_table(ht); } int main () { int A[] = {1 , 2 , 3 , 4 , 5 , 6 }; int N = sizeof (A) / sizeof (A[0 ]); int X = 7 ; find_pairs_with_hash(A, N, X); return 0 ; }
时间复杂度分析
构建哈希表的时间复杂度 :为 $ O(n) $,遍历数组并插入每个元素。
查找和的时间复杂度 :对于每个元素查找其补数,平均时间复杂度为 $ O(1) $。
整体时间复杂度 :总时间复杂度为 $ O(n) + O(n) = O(n) $。
空间复杂度 :使用了额外的哈希表,空间复杂度为 $ O(n) $。