第 3 章 线性表 ..3.1线性表概述 数据之间存在各种关系,其中存在一种简单的“前后相继”的关系,即A数据 是 B 数据的前驱, B 数据是 A 数据的后继。如果每个数据都有唯一的前驱元素和 唯一的后继元素,那么就称为线性存储结构,即线性表。 其中,第一个数据元素无前驱,最后一个数据元素无后继。数据元素之间的 关系是“一对一”的,适合存储在线性表中。线性表是一种简单、基本的数据结构, 应用场景较为广泛,可以用于存储和处理一组结构相同的数据元素,如一组学生 的考试成绩,也是后续章节中堆栈和队列的实现基础。 线性表有顺序和链表两种存储方式。顺序存储方式用数组实现,定义一个指 定长度的数组,将数据元素依次存储在数组中,使用数组下标可以访问到指定位 置的数据元素。链表存储方式创建一组带指针域和数据域的结构体,数据域中存 储数据元素,指针域中存储相邻数据元素的地址。 ..3.线性表的定义及基本操作 2 一个线性表是 n 个具有相同特性的数据元素的有限序列。数据元素可以是 简单数据类型,也可以是结构体、对象、指针、字符串等其他数据类型。但在同一 个线性表中,数据类型必须统一。在线性表中,出现在 A 数据前面的数据元素称 为 A 的“前驱元素”;出现在 A 的后面的元素称为 A 的“后继元素”。在线性表中, A 的前驱元素中最接近 A 的数据元素称为“直接前驱”; A 的后继元素中最接近 A 的数据元素称为“直接后继”。 线性表的基本操作如下。 创建空表:创建一个线性表,数据元素个数初始化为0。 插入:确定插入的位置,在线性表中插入一个元素,元素个数加1。 删除:找到要删除元素的位置,删除元素,元素个数减1。 查找:给出元素的值,返回元素在线性表中的位置,如果元素不存在则返 回空。 第3章 线性表 41 .. 3.3 线性表存储结构 线性表有两种存储结构,顺序存储结构将数据存储于连续的物理空间,通过下标确定数 据的位置;链式存储结构将数据存储于不连续的物理空间,通过指针确定后继数据的位置。 3.3.1 线性表的顺序存储结构 顺序存储结构用数组的形式实现。在C++中,定义一个数组的时候,系统为数组分配 了一个连续的物理空间。数组的名就是数组存储在内存中的首地址。线性表的数据元素的 数据类型就是数组数据类型,各个数据元素的数据类型相同。 数组的长度即为数组中可以存储的数据元素的个数。数组元素的下标可以表示数据元 素在线性表中的位置。 顺序存储结构的优点有:创建简单,访问效率高,可以通过下标从任意位置开始访问线 性表,查找前驱元素和后继元素都很方便。 3.3.2 线性表的链表存储结构 顺序结构在使用方便的同时也存在限制:数组大小确定,不能动态增长;存储的内存需 要连续的空间,随着数据元素的增加,可能无法开辟出足够的连续内存空间。而链表结构可 以解决顺序结构的这些限制。链表结构在需要存储时再动态开辟一个结点空间,结点空间 由以下部分组成。 数据域:每个结点存储的数据元素本身的信息。 指针域:元素之间逻辑关系的信息,后继指针存储后继结点的地址,前驱指针(可选)存 储前驱结点的地址。通过指针域可以找到相邻结点的地址,提高数据查找速度。 一般来说,每个结点有一个或多个指针域,若某个结点不存在前驱或者后继,对应的指 针域设为空,用常量NULL表示。 链表存储结构可以用于实现堆栈和队列,处理大规模数据,实现动态扩展和收缩的数据 结构,还可以用于Hash表解决冲突。 链表有单向链表、双向链表、循环链表等形式。 .. 3.4 线性表的实现 3.4.1 单链表 单链表 结点只包含指向后继结点的指针,适用于基本上只做单向访问的线性表。链表结点可 以用结构体来描述如下。 //定义单链表结点 struct ListNode { int val; 42 数据结构与问题求解(C++版·微课版) ListNode* next; ListNode(int x) : val(x), next(NULL) {} }; 其中,成员val用于存放结点中的有用数据,next是指针类型的成员,指向structListNode 类型数据,构建结点时可将指针域的默认值设为空。 链表头有两种设置模式:头结点模式和头指针模式。头结点模式中,head是一个结点, next域存储第一个结点的地址;头指针模式中,head是一个指针,存储第一个结点的地址, 如图3.1所示。 图3.1 头结点和头指针模式下的单链表 虽然直接在栈区创建结构体变量可以用于创建链表,但是不能体现链表动态改变长度 的优势,因此往往使用动态内存分配,将结点建立在堆区。下面使用动态内存分配创建一个 学生成绩链表,学生的信息包含学号和成绩,并在链表中加入4个学生的数据。 1.链表类的创建 首先创建链表结点结构体,设置一个头指针head指向链表的第一个元素。 struct Student { int id; //学号 float score; //成绩 Student* next; //指向下一个结点的指针 }; /*学生链表类*/ class LinkedList { private: Student* head; //指向链表的头指针 2.构造函数与析构函数 构造函数创建一个空的链表,此时链表中无结点,head指针的值为空。析构函数需要 清空链表,将堆区所占用的空间进行释放。head指针所指的数据不能直接释放,否则链表 后继数据将无法找到,需要借助一个temp指针指向head指针所指的第一个元素,head指 针再进行后移,释放掉temp指针所指的数据元素,循环进行,直至所有数据都被释放。 public: LinkedList() { head =NULL; } ~LinkedList() { Student* temp; 第3章 线性表 43 while(head !=NULL) { temp =head; head =head->next; delete temp; } } 3.头插法插入结点 使用头插法插入一个学生结点。首先创建一个新的学生结点,然后将学号和成绩赋给 结点数据域,结点指针域赋为head,即第一个元素的地址,最后将head指针指向新结点。 void add(int id, float score) { Student* newNode =new Student(); newNode->id =id; newNode->score =score; newNode->next =head; head =newNode; } 4.按序插入结点 按照给出的学号,找到适合的位置插入结点,使得学号按升序排列。首先生成新结点 newNode,然后使用访问指针p 从第一个元素开始进行比较,找到插入位置后终止循环,如 图3.2所示。 图3.2 按序插入结点 void insert(int id, float score) { Student* newNode =new Student(); newNode->id =id; newNode->score =score; newNode->next =NULL; if(head ==NULL) { head =newNode; } else { Student* p =head; //使用访问指针p 找到插入元素的位置 while(p->next !=NULL && p->next->id <newNode->id) { p =p->next; 44 数据结构与问题求解(C++版·微课版) } newNode->next =p->next; p->next =newNode; } } 5.删除结点 删除结点首先要查找到指定结点,使用一个访问指针p 对链表进行遍历,比对结点的 id值进行查找。对结点进行删除后,要将前后的两个结点进行连接,因此需要借助一个前 驱指针prev来保存左侧结点的地址。如果删除的结点是第一个元素,那么在查找中p 指针 没有移动,prev指针的值为空,此时只需要将head指针指向第二个元素,再释放p 指针即 可。假设要删除的是学号为103的学生,如图3.3所示。 图3.3 删除一个结点 void remove(int id) { Student* p =head; //设置访问指针指向第一个元素 Student* prev =NULL; //设置前驱指针指向访问指针前一个元素 while(p !=NULL && p->id !=id) //查找id 对应的结点 { prev =p; p =p->next; } if(p ==NULL) //没有找到对应的学号 { cout <<"Student not found." <<endl; return; } if(prev ==NULL) //id 找到的是第一个元素 { head =p->next; } else //其余的位置 { prev->next =p->next; } delete p; //释放要删除的元素 } 第3章 线性表 45 3.4.2 双向链表 双向链表在每个结点中除包含数值域外,设置有两个指针域,分别用于指向其前驱结点 和后继结点,这样构成的链表称为线性双向链表,简称双链表。 在双链表中具有两个指针,所以当访问过一个结点后,既可以依次向后访问每个结点, 也可以依次向前访问每个结点,如图3.4所示。 图3.4 双向链表 双向链表可以设计一个头指针head指向第一个元素,一个尾指针tail指向最后一个元 素,创建双向链表时将头尾指针都赋为空。双向链表中增加了一个指针域,成员函数中要增 加相应的处理。 #include <iostream> using namespace std; struct Node //链表结点结构体 { int data; Node* prev; //前驱指针 Node* next; //后继指针 Node(int d) //初始化结点 { data =d; prev =NULL; next =NULL; } }; class DoublyLinkedList //双向链表类 {p ublic: Node* head; //头指针 Node* tail; //尾指针 DoublyLinkedList() { head =NULL; tail =NULL; } void append(int data) //尾插插入元素 { Node* newNode =new Node(data); if(head ==NULL) { head =newNode; tail =newNode; 46 数据结构与问题求解(C++版·微课版) } else { tail->next =newNode; newNode->prev =tail; tail =newNode; } } void prepend(int data) //头插插入元素 { Node* newNode =new Node(data); if(head ==NULL) { head =newNode; tail =newNode; } else { head->prev =newNode; newNode->next =head; head =newNode; } } void print() //打印双向链表 { Node* p =head; while(p !=NULL) { cout <<p->data <<" "; p =p->next; } cout <<endl; } }; 3.4.3 循环链表 循环链表形成环状结构,有head、tail两个固定指针,tail结点的next指针指向第一个 结点,在双向的循环链表中,head的prev指针指向最后一个结点,这样将链表连接为环状 结构。从表中任一结点出发均可找到链表中的其他结点。循环链表的结构如图3.5所示。 双向循环链表可以由头指针找到尾指针,类中也可以不设置tail指针。 循环链表形成的环状结构可以解决环状结构的问题,如约瑟夫环问题。 第3章线性表47 图3.5循环链表 ..3.5能力拓展 3.5.1判断链表中是否存在环 判断链表中 是否存在环 给定一个单链表,其中可能存在某个结点的next指针指向前面已经出现过的结点,此 时链表中出现了环,如果使用访问指针直接进行遍历则无法达到链表的尾端。给出一个链 要求返回链表开始入环的第一个结点。从链表的头结点开始沿着nt指针进 表的头指针, ex 入环的第一个结点为环的入口结点。如果链表无环,则返回NULL 。 为了表示给定链表中的环,使用整数pos来表示链表尾连接到链表中的位置(索引从0 开始)。如果pos是-1,则在该链表中没有环。注意,pos仅用于标识环的情况,并不会作 为参数传递到函数中。不允许修改给定的链表。 如表3. 1所示为判断链表是否有环的示例。 表3.判断链表是否有环的示例 1 示例 1 示例 2 示例 3 head=[1,4,2,7,5],pos=1 head=[1,4,2,7,5],pos=0 head=[1,4,2,7,5],pos=-1 返回索引为1的链表结点返回索引为0的链表结点返回NULL 链表中有一个环,其尾部连接到 第2个结点 链表中有一个环,其尾部连接到 第1个结点 链表中不存在环 解题思路: 使用一个慢指针slow 和一个快指针fast判断链表中是否存在环。两个指针都从head 指针开始对链表进行访问,ow 指针每次移动一个结点,t指针每次移动两个结点。如 slfas 果链表不存在环,那么fas如图3. t指针将先访问到NULL, 6所示 。 当链表存在圈时,t指针和sow 指针会相遇,设sow 指针走过的结点个数为s, fasllfast 指针走过的结点个数是2s。设链表的圈外结点个数为Nout,圈内结点个数为Nin,圈内 48 数据结构与问题求解(C++版·微课版) 图3.6 使用快慢指针判断是否有环 slow指针走过的结点个数为Sv,如图3.7所示。 图3.7 快慢指针相遇时的情况 当快慢指针相遇时,慢指针走过的结点数目: s=Nout+Sv (1) 快指针走过的结点数目(n 为快指针走过的圈数): 2s=Nout+n ×Nin+Sv (2) 由(1)(2)式联立得Nout=n×Nin-Sv,即圈外结点的数目Nout和从相遇点出发共绕 n 圈回到入圈点的距离相等。slow 指针此时停留在相遇点,因此只需要让slow 指针走 Nout步就能刚好回到圈的入口点。可以再设置一个指针p 从head出发进行访问,当p 和 slow相遇时,p 走过了Nout步,slow回到入口点,即求得圈的入口点。 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode*next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode*detectCycle(ListNode*head) { if(head==NULL||head->next==NULL)//特殊情况 return NULL; ListNode* fast=head; ListNode* slow=head; do{ if(fast==NULL||fast->next==NULL) return NULL; 第3章 线性表 49 fast=fast->next->next; slow=slow->next; }while(fast!=slow); //退出循环时快慢指针相遇了 ListNode*p=head; //设置指针从head 出发 while(p!=slow) //p 和slow 将相遇在入圈点 { p=p->next; //两指针移动 slow=slow->next; } return slow; //返回入圈点的地址 } }; 3.5.2 约瑟夫环 约瑟夫环问题:有num 只猴子,按顺时针方向围成一圈选大王(编号从1到num),从 第1号开始报数,一直数到target,数到target的猴子退出圈外,剩下的猴子再接着从1开 始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入num 和 target后,输出最后猴王的编号。 解题思路: 约瑟夫环是学习算法的一个经典问题,可以使用数组、指针、链表和数学方法解决。我 们用循环链表模拟猴子报数的结构。每只猴子都看成一个结点,从1号到num 号依次加入 链表中。链表head指针指向1号,num 号猴子的next指针指向head,使得猴子能形成环状 结构,示例如图3.8所示。 图3.8 6只猴子进行报数,报到2出圈,最后剩下5号 用指针cur从表头开始对链表进行访问模拟报数的过程,每跳一个结点进行一次报数,报 到target-1时删除cur指针所指的下一个结点,最后只剩下一个结点时输出结点的编号。 struct Node { int val; Node* next; Node(int x) : val(x), next(nullptr) {} }; 50 数据结构与问题求解(C++版·微课版) int josephus(int num, int target) { //创建循环链表 Node* head =new Node(1); Node* cur =head; for(int i =2; i <=num; i++) { cur->next =new Node(i); cur =cur->next; } cur->next =head; //形成循环 //当圈内个数大于1 时 while(num >1) { //进行报数 for(int i =1; i <target; i++) { cur =cur->next; } //删除结点 Node* tmp =cur->next; cur->next =tmp->next; delete tmp; num--; //圈内数目减1 } //返回最后剩下的结点编号 return cur->val; } .. 习 题 1.有带有字母A、B、C的三张卡片,按某种顺序排列在一排。最多可以进行一次操作, 选择两张牌并交换它们。是否操作后可以变成ABC? 是就输出“YES”,否就输出“NO”。 输入格式:第一行包含一个整数t(1≤t≤10),代表有t 组数据。接下来n 行,每行包 括一个长度为3的字符串,由A、B、C三个字符组成。 输出格式:“YES”或者“NO”。 2.小S有一个数组,他想检查这个数组是否是一个排列。一个长度为n 的排列指的 是,1-n 在这个排列中都出现且仅出现过一次。 输入格式:第一行输入一个整数n。第二行输入n 个整数,表示数组元素。 输出格式:“YES”或者“NO”。 3.众所周知,小S十分挑食。这天,小S又来到了食堂,但他的挑食症又犯了,他不会 选择热量为k 的食物,请问他有多少种选择? 输入格式:第一行输入一个整数t,代表有多少组数据。第二行输入两个整数n 和k,n 代表有多少个食物,不同食物的热量可能相同。第三行输入n 个整数ai,ai 表示每个食物 的热量。(1≤t≤104,1≤n≤103,1≤ai,k≤109。) 第3章线性表51 输出格式:他有多少种选择,每组数据输出一个整数占一行。 4.给定两个大小分别为m和n的数组a和b。请找出并输出这两个数组的中位数。 输入格式:第一行输入两个整数n和m。第二行输入n个整数,表示数组a。第三行 输入m个整数,表示数组b。 输出格式:请输出一个浮点数,保留一位小数。 5.森林里有n只猴子,它们都想当猴王,因为当猴王就不用自己找果子吃。但不是每 只猴子都能当猴王,能当上猴王的只能有一只,于是便有了这样的规则:n只猴子围成一个 圈,从第1只猴子开始报数1,2,3,…报出m的那只猴子出列,失去继续争夺猴王的机会,由 下一只猴子继续从1开始报数,最终只剩下一只猴子得胜成为猴王。 输入格式:输入两个整数n和m。 输出格式:输出一个整数———第几只猴子获胜成为猴王。 6. 小S最近在学习后缀表达式,他已经把后缀表达式练得炉火纯青了,这一天他又看 到了一道后缀表达式的题,他觉得太简单了,于是交给了你。后缀表达式,指的是不包含括 号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行 (不再考虑运算符的优先规则) 。将给出一个后缀表达式T,保证T的长度不超过100,且操 作数仅为0~9的整数,操作符只包含+、-、*,求该后缀表达式的计算结果。 输入格式:本题的输入为单个测试用例,输入长度不超过100,且操作数仅为0~9的整 数,操作符只包含+、-、*等算术运算符的中缀表达式。 输出格式:输出一个整数,为该后缀表达式的计算结果。 7. 斯拉夫正在为朋友的生日准备礼物,礼物是 n 个数字的乘积。斯拉夫有一次机会选 择 n 个数字中的一个数字来增加1。请问斯拉夫可以送给朋友的最大礼物数是多少? 输入格式:第一行包含一个整数t(1≤t≤104)测试用例的数量。每个测试用例的第一 行包含一个整数n(1≤n≤9 )。每个测试用例的第二行包含 n 个空格分隔的整数ai (0≤ ai≤9 )。 输出格式:对于每个测试用例,输出一个整数。 j! 8.给你一个整数数组a, i],j],k], =i! j] 判断是否存在三元组a[a[a[满足i! j,= k, k= ] k = 并且a[=a[a[=a[a[=a[同时还满足a[ i]! j],i]! k],j]! k], i]+a[ +a[0。请统计三元组的数量。 输入格式:第一行输入一个整数n,数组 a 的长度。第二行输入 n 个整数。 输出格式:输出一个整数,为三元组的数量。 9.给定 n 个整数a1,a求问有多少个四元组(j,l) a2,…, n , i,k,满足以下条件:1≤i< j <k<l≤n,ai=aj =。 ak ,al 输入格式:第一行输入一个整数t,表示数据组数。对于每组数据,下一行输入一个整 数n,表示有多少个数。对于每组数据, 表示a1,an 。 下一行输入 n 个整数, a2,… , 输出格式:每组输出一个整数,为四元组的数量 。 10.小李在玩一个游戏,他有 n 个盒子,每个盒子有两个数字,小李自己也有两个数字。 每个盒子可以得到的分数,是排在这个盒子前面的所有盒子的第一个数的乘积,除以这个盒 52数据结构与问题求解(C++版·微课版) 子的第二个数,然后向下取整得到的结果。小李不希望某一个盒子得到特别多的分数,所以 他想请你帮他重新安排一下盒子的顺序,使得得到分数最多的盒子,所获分数尽可能的少。 注意,小李的位置始终在盒子队列的最前面。 输入格式:第一行包含一个整数n,表示盒子的数量。第二行包含两个整数a和b,之 间用一个空格隔开,分别表示小李的两个数。接下来n行,每行包含两个整数a和b,之间 用一个空格隔开,分别表示每个盒子的两个数。 输出格式:一个整数,表示重新排列后的盒子队列中获分数最多的盒子所获得的分数。