链表不具有的特点是( )。
C:可随机访问任一元素。链表靠指针顺序链接,无法按下标 O(1) 定位,不能随机访问;A(空间与长度成正比)、B(插入删除不移动元素)、D(不需预估空间)均为链表的优点。
链表和数组的区别包括( )。
C:数组大小固定,链表大小可动态调整。数组声明时大小固定,链表按需增删;A 错(数组可排序),B 错(存储量取决于元素类型,非结构本身)。
假设有一个链表的节点定义如下:
01struct Node { 02 int data; 03 Node* next; 04};
现在有一个指向链表头部的指针:Node* head。如果想要在链表中插入一个新节点,其成员 data 的值为 42,并使新节点成为链表的第一个节点,下面哪个操作是正确的?( )
A:创建新节点、置 data=42、next 指向原 head、更新 head 为新节点。B 错(误改 head->data 而非新节点)、C 错(误插在 head 后而非头部)。
在 C++ 中,通过恰当的实现,可以将链表首尾相接,形成循环链表。
对。构造时把尾节点的 Next 指针指向头节点即可首尾相接形成循环链表,从任一节点出发都能绕环遍历回到自身,无需额外数据结构。
如果将双向链表的最后一个结点的下一项指针指向第一个结点,第一个结点的前一项指针指向最后一个结点,则该双向链表构成循环链表。
对。把尾结点 next 指向头结点、头结点 prev 指向尾结点后,沿 next 或 prev 都能从任一结点绕回自身,构成双向循环链表。
数组和链表都是线性表,链表的优点是插入删除不需要移动元素,并且能随机查找。
错。链表插入删除确实不需移动元素,但只能从头结点沿 next 顺序查找,不支持随机访问;数组按下标才能 O(1) 随机访问。
链表的存储空间物理上可以连续,也可以不连续。
对。链表结点由 new 逐个分配,物理地址既可连续也可分散,靠 next、prev 指针串联成逻辑顺序,并不要求像数组那样整体连续存储。
下面关于链表和数组的描述,错误的是( )。
C:链表每个结点除存数据外还要额外存 next 指针,存相同数目整数时链表占的内存更多;数组只存数据本身,故 C 把两者说反了,应为链表比数组所需内存更多。
在循环单链表中,节点的 next 指针指向下一个节点,最后一个节点的 next 指针指向( )。
C:循环单链表首尾相接,最后一个节点的 next 指向第一个节点,链成环便于从任一节点绕回头部;指向 nullptr 或当前节点则退化为普通链表。
单链表只支持在表头进行插入和删除操作。
错。只要拿到前驱节点指针,单链表就能在任意位置插入或删除,开销 O(1);只有在从头找位置时才需 O(n),并非只允许在表头操作。
链表不具备的特点是( )。
A:链表结点靠 next 指针链接,访问第 i 个元素必须从头结点逐个遍历,无法随机访问;插入删除只改指针、结点动态申请、存储量与结点数成正比,都是链表具备的特点。
链表存储线性表时要求内存中可用存储单元地址是连续的。
错。链表结点用 new 逐个动态分配,各结点在内存中的地址可以任意分散,靠 next/prev 指针把逻辑相邻的结点串起来即可,不要求存储单元地址连续;要求地址连续的是数组。
与数组相比,链表在( )操作上通常具有更高的效率。
C:链表在已知位置插入或删除只需修改相邻节点的指针,与节点个数无关;数组则要移动该位置后的所有元素。随机访问、查找指定元素、遍历这三项数组反而更快。
以下哪种情况使用链表比数组更合适?
B:链表结点靠 next 指针相连,无需连续存储,在中间或开头插入删除只改指针,而数组要移动后续元素。读多写少、随机访问、要求连续存储都更适合数组。
以下哪组操作能完成在双向循环链表结点 p 之后插入结点 s 的效果(其中,next 域为结点的直接后继,prev 域为结点的直接前驱):( )。
D:s->next = p->next; p->next->prev = s; s->prev = p; p->next = s。顺序:先连 s 的 next 和 prev,再断 p 与 p->next 重连 p 与 s,保证不丢失 p 原来的后继指针。
有关下面代码的说法正确的是( )。
01#include <iostream> 02 03class Node { 04public: 05 int Value; 06 Node * Next; 07 08 Node(int Val, Node * Nxt = nullptr) { 09 Value = Val; 10 Next = Nxt; 11 } 12}; 13 14int main() { 15 Node * firstNode = new Node(10); 16 firstNode->Next = new Node(100); 17 firstNode->Next->Next = new Node(111, firstNode); 18 return 0; 19}
C:循环链表。第三节点用 new Node(111, firstNode) 把 Next 指向 firstNode,尾节点指回头节点,首尾相接成环;每节点只有一个 Next 指针,故非双向链表。
有关下面 C++ 代码的说法正确的是( )。
01class Node 02{ 03public: 04 int Value; 05 Node* Prev; 06 Node* Next; 07 Node(int Val, Node* Prv = NULL, Node* Nxt = NULL); 08}; 09 10Node::Node(int Val, Node*Prv, Node* Nxt) 11{ 12 this->Value = Val; 13 this->Prev = Prv; 14 this->Next = Nxt; 15} 16 17int main() 18{ 19 Node firstNode = Node(10); 20 firstNode.Next = new Node(100, &firstNode); 21 firstNode.Next->Next = new Node(111, firstNode.Next); 22}
B:firstNode 的 Prev、Next 均为 NULL,第二个节点 Prev 指向 firstNode,第三个节点 Prev 指向第二个节点,三个节点通过 Next 依次相连、两端为 NULL,构成三节点双向链表而非循环链表。
同样的整数序列分别保存在单链表和双向链中,这两种链表上的简单冒泡排序的复杂度相同。
对。单链表与双向链表上简单冒泡排序的比较、交换次数相同,复杂度均为 O(N²);单链表找前驱更麻烦些只影响常数倍,不改变复杂度。
下面的代码片段用于在双向链表中删除一个节点。请在横线处填入( ),使其能正确实现相应功能。
01void deleteNode(DoublyListNode*& head, int value) { 02 DoublyListNode* current = head; 03 while (current != nullptr && current->val != value) { 04 current = current->next; 05 } 06 if (current != nullptr) { 07 if (current->prev != nullptr) { 08 ____________ // 在此处填入代码 09 } else { 10 head = current->next; 11 } 12 if (current->next != nullptr) { 13 current->next->prev = current->prev; 14 } 15 delete current; 16 } 17}
B:current 非头结点时让前驱的 next 跳过:current->prev->next = current->next;后面代码处理 current->next->prev = current->prev 再 delete current。A 与已有行重复,C 误删后继,D 改错指针方向。
单链表和双链表都可以在常数时间内实现在链表头部插入或删除节点的操作。
对。头插只需新建节点使其 next 指向原 head 再更新 head(双链表再补 prev),头删直接 head=head->next 并处理 prev,均无需沿链表查找,所以是常数时间 O(1)。
小杨采用如下双链表结构保存他喜欢的歌曲列表:
01struct dl_node { 02 string song; 03 dl_node* next; 04 dl_node* prev; 05};
小杨想在头指针为 head 的双链表中查找他喜欢的某首歌曲,采用如下查询函数,该操作的时间复杂度为( )。
01dl_node* search(dl_node* head, string my_song) { 02 dl_node* temp = head; 03 while (temp != nullptr) { 04 if (temp->song == my_song) 05 return temp; 06 temp = temp->next; 07 } 08 return nullptr; 09}
B:O(n)。search 从头指针 head 出发,每轮把 temp->song 与 my_song 比较,不相等就沿 temp=temp->next 前进,最坏需遍历全部 n 个结点才返回结果,故为 O(n)。
通过( )操作,能完成在双向循环链表结点 p 之后插入结点 s 的功能(其中 next 域为结点的直接后继,prev 域为结点的直接前驱)。
D:正确顺序是先连后断:先令 s 的 next 指向 p 原来的后继,再令那个后继的 prev 指向 s,接着令 s 的 prev 指向 p,最后才把 p 的 next 改成 s;其余选项先后颠倒,会使 s 的 next 或 prev 指向 s 自身,链表被破坏。
在操作系统中,需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循环操作可以通过环形链表来实现。
对。操作系统的时间片轮转按固定顺序循环切换进程,环形链表的尾结点 next 指向头结点,走完一圈能自动回到第一个进程,周而复始,恰好支持这种循环调度。
为了方便链表的增删操作,一些算法生成一个虚拟头节点,方便统一删除头节点和其他节点。下面代码实现了删除链表中值为 val 的节点,横线上应填的最佳代码是( )。
01struct LinkedNode { 02 int val; 03 LinkedNode* next; 04 LinkedNode(int val):val(val), next(nullptr){} 05}; 06 07void removeElements(LinkedNode* head, int val) { 08 if (head == nullptr) { 09 return; 10 } 11 LinkedNode* cur; 12 LinkedNode* dummyHead = new LinkedNode(0); //虚拟头节点 13 ____________ // 在此处填入代码 14 15 while(cur ->next ! = nullptr) { 16 if(cur->next->val == val) { 17 LinkedNode* tmp = cur->next; 18 cur->next = cur->next->next; 19 delete tmp; 20 tmp = nullptr; 21 } 22 else { 23 cur = cur ->next; 24 } 25 } 26 head = dummyHead->next; 27 delete dummyHead; 28 dummyHead = nullptr; 29}
A:dummyHead->next = head 保留原链表,cur = dummyHead 让循环从虚拟头开始,才能统一处理头节点等于 val 的情况;其余选项把 head->next 赋给 dummyHead 会丢掉头节点。
二分查找仅适用于数组而不适合链表,因为二分查找需要跳跃式访问元素,链表中执行跳跃式访问的效率低。
对。二分查找每轮要取中点做 O(1) 随机访问,数组支持下标直达;链表只能从头遍历到中点,跳跃访问 O(n),故不适合。
双向链表中每个结点有两个指针域 prev 和 next,分别指向该结点的前驱及后继结点。设 p 指向链表中的一个结点,它的前驱结点和后继结点均非空。要删除结点 p,则下述语句中错误的是( )。
A:p->next->prev=p->next 把后继结点的前驱指向自己,p->prev->next=p->prev 把前驱结点的后继也指向自己,两条链都没接到对方;应让 p 的前驱与后继互相连接,如 B 的 p->prev->next=p->next、p->next->prev=p->prev。
假设双向循环链表包含头尾哨兵结点(不存储实际内容),分别为 head 和 tail,链表中每个结点有两个指针域 prev 和 next,分别指向该结点的前驱及后继结点。下面代码实现了一个空的双向循环链表,横线上应填的最佳代码是( )。
01// 链表结点 02template <typename T> 03struct ListNode { 04 T data; 05 ListNode* prev; 06 ListNode* next; 07 // 构造函数 08 explicit ListNode(const T& val = T()) 09 : data(val), prev(nullptr), next(nullptr) {} 10}; 11 12struct LinkedList { 13 ListNode<T>* head; 14 ListNode<T>* tail; 15}; 16 17void InitLinkedList(LinkedList* list) { 18 list->head = new ListNode<T>; 19 list->tail = new ListNode<T>; 20 ____________ // 在此处填入代码 21}
B:空的双向循环链表只有 head、tail 两个哨兵,应令 head->next=tail、tail->prev=head,使二者互相指连成环且中间无数据结点;A 只连 prev,C 的 tail->next 指向 head 但缺 prev 连接,D 把 tail->next 置空破坏了循环。
要删除单链表中某个结点 p(非尾结点),但不知道头结点,可行的操作是将 p->next 的数据拷贝到 p 的数据,将 p->next 设置为 p->next->next,然后删除 p->next。
对。把后继结点 p->next 的数据拷入 p 覆盖原值,再令 p->next 指向 p->next->next 并删除原后继,等效删除 p 本身;因 p 非尾结点,后继必存在,全程只需 p 一个指针,无需头结点。
下面 C++ 代码实现双向链表。函数 is_empty() 判断链表是否为空,如果链表为空返回 true,否则返回 false。横线处不能填写( )。
01// 节点结构体 02struct Node { 03 int data; 04 Node* prev; 05 Node* next; 06}; 07 08// 双向链表结构体 09struct DoubleLink { 10 Node* head; 11 Node* tail; 12 int size; 13 14 DoubleLink() { 15 head = nullptr; 16 tail = nullptr; 17 size = 0; 18 } 19 20 ~DoubleLink() { 21 Node* curr = head; 22 while (curr) { 23 Node* next = curr->next; 24 delete curr; 25 curr = next; 26 } 27 } 28 29 // 判断链表是否为空 30 bool is_empty() const { 31 ____________ 32 } 33};
C:head 是指针不是对象,不能写 head.data,且 data 为 0 也不能表示空链表。head==nullptr、tail==nullptr、size==0 三种判法在空链表时均为真,A、B、D 都可用。
在双向链表尾部增加新节点的 append() 函数中,横线上应填写( )。
01void append(int data) { 02 Node* newNode = new Node(data, nullptr, nullptr); 03 04 if (is_empty()) { 05 head = tail = newNode; 06 } else { 07 ____________ 08 } 09 ++size; 10}
D:先 tail->next=newNode 连接旧尾与新节点,再 newNode->prev=tail 设置新节点前驱,最后 tail=newNode 更新尾指针。A 缺两条指针更新,B 顺序错误,C 先改 tail 导致连错节点。
函数 removeElements 删除单链表中所有结点值等于 val 的结点,并返回新的头结点,其中链表头结点为 head,则横线处填写( )。
01// 结点结构体 02struct Node { 03 int val; 04 Node* next; 05 Node() : val(0), next(nullptr) {} 06 Node(int x) : val(x), next(nullptr) {} 07 Node(int x, Node *next) : val(x), next(next) {} 08}; 09 10Node* removeElements(Node* head, int val) { 11 Node dummy(0, head); // 哑结点,统一处理头结点 12 Node* cur = &dummy; 13 while (cur->next) { 14 if (cur->next->val == val) { 15 ____________ // 在此填入代码 16 } 17 else { 18 cur = cur->next; 19 } 20 } 21 return dummy.next; 22}
C:先令 del 指向 cur->next 即待删结点,把 cur->next 改为 del->next 跳过它,再 delete del。D 先 delete 后访问 del->next 属悬垂指针,A、B 均未真正摘除结点。
链表通过更改指针实现高效的结点插入与删除,但结点访问效率低、占用内存较多,且对缓存利用不友好。
对。链表结点靠指针链接、内存不连续,插入删除只改指针因而高效,但按序访问只能逐个跳 next,且每个结点多存指针、对缓存不友好,描述准确。
对如下定义的循环单链表,横线处填写( )。
01// 循环单链表的结点 02struct Node { 03 int data; // 数据域 04 Node* next; // 指针域 05 06 Node(int d) : data(d), next(nullptr) {} 07}; 08 09// 创建一个只有一个结点的循环单链表 10Node* createList(int value) { 11 Node* head = new Node(value); 12 head->next = head; 13 return head; 14} 15 16// 在循环单链表尾部插入新结点 17void insertTail(Node* head, int value) { 18 Node* p = head; 19 while (p->next != head) { 20 p = p->next; 21 } 22 Node* node = new Node(value); 23 node->next = head; 24 p->next = node; 25} 26 27// 遍历并输出循环单链表 28void printList(Node* head) { 29 if (head == nullptr) return; 30 31 Node* p = head; 32 ____________ // 在此处填入代码 33 cout << endl; 34}
C:循环链表尾结点的 next 指回 head,p 永远非空,须用 do-while 先输出 head 再后移,直到 p 回到 head 停止;A/B/D 的退出条件依赖 nullptr 永不成立,会死循环。
个节点的双向循环链,在其中查找某个节点的平均时间复杂度是( )。
B:O(N)。链表没有随机访问能力,查找必须从某结点出发逐结点比较;双向循环链表只是双向可走、首尾相连,平均仍要查约 N/2 个结点,平均时间复杂度 O(N)。
有关下面 C++ 代码的说法正确的是( )。
01typedef struct LinkList { 02 int data; 03 04 LinkList* next; 05 06 LinkList* prev; 07 08}LinkList,LinkNode; 09bool ListInit(LinkList* &L) { 10 11 L = new LinkNode; 12 if (!L)return false; 13 14 L->next = NULL; 15 L->prev = NULL; 16 L->data = -1; 17 18 return true; 19 20}
B:LinkList 结点同时含 next 与 prev 两个指针,分别指向后继与前驱,构成双向链表;不是单向链表、循环链表,也不存在独立的「指针链表」类型。