--- title: "《数据结构》线性表" date: 2022-11-16T21:53:40+08:00 --- ## 线性表的定义和操作 ### 定义 线性表是具有相同数据类型的$n (n \leq 0 )$ 个元素的有限序列,其中$n$为表长,当 $n = 0$时线性表为一个空表 若用$L$来命名线性表,则其一般表示为 $L=(a_1,a_2,\cdots,a_i,a_(i+1),\cdots,a_n)$ 式中,$ a_1 $是唯一的“第一个”数据元素,即表头元素;$a_n$是唯一的“最后一个”数据元素,即表尾元素 线性表的逻辑特性:每个元素有且仅有一个直接前驱。除去最后一个元素外,每个元素有且仅有一个直接后继,这种逻辑特性即为线性表的名称由来 线性表的特点: - 表中元素个数有限 - 表中元素具有逻辑上的顺序性,表中元素有其先后次序 - 表中元素都是数据元素,每个元素都是单个元素 - 表中元素的数据类型都相同,意味着每个元素占有的存储空间相同 - 表中元素具有抽象性,仅讨论元素间的逻辑关系,而忽视元素的实际内容 ### 基本操作 - InitList(&L):初始化表。构造一个空的线性表 - Length(L):求表长。返回线性表L的长度,即L中数据元素的个数 - LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素 - GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值 - ListInsert(&L,i,&e):插入操作。在表L中第i个位置插入指定元素e - ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值 - PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值 - Empty(L):判空操作。若L为空表,则返回true - DestroyList:销毁操作。销毁线性表,并释放线性表L所占用的内存空间 ## 线性表的顺序表示(顺序表) ### 定义 顺序表,即线性表的顺序存储。是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。 第一个元素存储在线性表的起始位置,第i个元素的存储位置后面紧接着存储的是$i + 1$个元素,称$i$为元素$a_i$在线性表中的位序,因此可见,顺序表的特点是表中元素的逻辑顺序与其物理顺序相同 假设线性表L存储的起始位置为LOC(A),sizeof(ElemType)为每个数据元素所占用存储空间的大小,可得该表对应的顺序存储如下 | 数组下标 | 顺序表 | 内存地址 | | :---: | :---: | :---: | | 0 | $a_1$ | LOC(A) | | 1 | $a_2$ | LOC(A) + sizeof(ElemType) | | i-1 | $a_i$ | LOC(A) + (i-1)$\times$sizeof(ElemType) | | n-1 | $a_n$ | LOC(A) + (n-1)$\times$sizeof(ElemType) | | MaxSize - 1 | $\cdots$ | LOC(A) + (MaxSize - 1)$\times$sizeof(ElemType) | 每个数据元素的存储位置和线性表的起始位置相差一个和该数据元素的位序成正比的常数,因此,线性表中的任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构 - Attention : 线性表中元素的位序从1开始,而数组中元素下标是从0开始的 假定线性表的元素类型为ElemType,则线性表的顺序存储类型可描述为: ```C++ #define MaxSize 50 //定义线性表最大长度 typedef struct{ ElemType data[MaxSize]; //顺序表的元素 int length; //顺序表的当前长度 }SqList; //顺序表的类型定义 ``` 一组数组可以是静态分配的,也可以是动态分配的 1. 静态分配时,由于数组的大小和空间已经固定,一旦空间占满,再加入新的数据就会产生溢出,进而导致程序崩溃 2. 动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满就另外开辟一块更大的存储空间用以替换原来的存储空间,从而扩充存储数组空间的目的,不需要为线性表一次性划分所有空间 动态分配的顺序存储类型定义: ```c++ #define InitSize 100 //表长度的初始定义 typedef struct{ ElemType *data; //指示动态分配数组的指针 int MaxSize,length; //数组的最大容量和当前个数 }SeqList; ``` C的初始动态分配语句为: ```c L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize); ``` C++的初始动态分配语句为: ```C++ L.data = new ElemType[InitSize]; ``` - Attention : 动态分配并非链式存储,同样属于顺序存储结构,物理结构无变化,依然为随机存储方式,只是分配的空间大小可以在运行时动态决定 - 顺序表主要特点是随机访问,即通过首地址和元素序号可以在时间$O(1)$内找到指定元素 - 顺序表存储密度相对更高,结点只存储数据元素 - 顺序表逻辑上相邻的元素物理上也相邻,所以在插入和删除时需要移动大量元素 ### 顺序表基本操作的实现 #### 插入 在顺序表L的第$i(1<=i<=L.length+1)$个位置插入新元素e。若i的输入不合法,则返回false,表示插入失败;否则,将第i个元素及其以后的所有元素依次往后移动一个位置,腾出一个位置插入新元素e,顺序表长度+1,插入成功,返回true ```c++ bool ListInsert(SqList &L,int i,ElemType e){ if(i<1||i>L.length+1) return false; if(L.length>=MaxSize) return false; for(int j=L.length;j>=i;j--) L.data[j]=L.data[j-1]; L.data[i-1]=e; L.length++; return true; } ``` - 最好情况:在表尾插入(即$i=n+1$),元素后移语句不执行,时间复杂度为$O(1)$ - 最坏情况:在表头插入(即$i=1$),元素后移语句将执行n次,时间复杂度为$O(n)$ - 平均情况:假设$p_i(p_i=1/(n+1))$是在第$i$个位置上插入一个结点的概率,则在长度为$n$的线性表中插入一个结点时,所需移动结点的平均次数为 $$ \sum_{i=1}^{n+1} p_i(n-i+1) = \sum_{i=1}^{n+1} \frac{1}{n+1}(n-i+1)=\frac{1}{n+1}\sum_{i=1}^{n+1}(n-i+1)=\frac{1}{n+1}\frac{n(n+1)}{2}=\frac{n}{2} $$ 由此可得,线性表插入算法的平均时间复杂度为$O(n)$ #### 删除 删除顺序表L中第$i$个$(1<=i<=L.length)$个位置的元素,用引用变量e返回。若i的输入不合法,则返回false;否则,将被删元素赋给引用变量e,并将第$i+1$个元素及其后的所有元素依次往前移动一个位置,返回true。 ```c++ bool ListDelete(SqList &L,int i,Elemtype &e){ if(i<1||i>L.length) //判断i的范围是否有效 return false; e=L.data[i-1]; //将被删除的元素赋值给e for(int j=i;j=t||L.length==0) return false; for(i=0;i<=L.length&&L.data[i]=L.length) return false; for(j=i;j=t) return false; //线性表为空或s,t不合法 for(i=0;i=s&&L.data[i]<=t) k++; else L.data[i-k]=L.data[i]; //当前元素前移k个位置 } L.length-=k; //长度减小 return true; } ``` 6. 从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同 算法思想:有序顺序表,因此值相同的元素一定在连续的位置上,用类似于直接插入排序的思想,初始时将第一个元素视为非重复的有序表,之后依次判断后面的元素是否与前面非重复有序表的最后一个元素相同,若相同,则继续向后判断,若不同,则插入前面的非重复有序表的最后,知道判断到表为为止 ```c++ bool Delete_Same(SeqList& L){ if(L.length==0){ return false; int i,j; for(i=0,j=1;jC.maxSize) return false; int i=0,j=0,k=0; while(i=right||right>=arraySize) return; int mid=(left+right)/2; for(int i=0;i<=mid-left;i++){ Datatype temp = A[left+i]; A[left+i]=A[right-i]; A[right-i]=temp; } } void Exchange(DataType A[],int m,int n,int arraySize){ Reverse(A,0,m+n-1,arraySize); Reverse(A,0,n-1,arraySize); Reverse(A,n,m+n-1,arraySize); } ``` 9. 线性表$(a_1,a_2,a_3,\cdots,a_n)$中的元素递增有序且安顺序存储于计算机内,要求设计一个算法,完成用最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素仍递增有序 算法思想:顺序存储的线性表递增有序,可以顺序查找,也可以折半查找,题目要求"用最少的时间在表中查找数值为x的元素",这里应使用折半查找法 ```c++ void SearchExchangeInsert(EkemType A[],ElemType x){ int low=0,high=n-1,mid; //low和high指向顺序表的上界和下界的下标 while(low<=high){ mid = (low+high)/2; //找中间位置 if(A[mid]==x) //找到x,退出while循环 break; else if(A[mid]high){ //查找失败,插入数据元素x for(i=n-1;i>high;i--) //后移元素 A[i+1]=A[i]; A[i+1]=x; //插入x } } ``` 10. 设将$n(n>1)$个整数存放到一维数组R中,设计一个在时间和空间两方面都尽可能高效的算法,将R中保存的序列循环左移$p(0b,则舍弃序列A中较大的一半,同时舍弃序列B中较小的一半,要求两次舍弃的长度相等。 在保留的两个升序序列中,重复过程1,2,3,直到两个序列中均只含有一个元素时为止,较小者即为所求的中位数 2)采用C或C++或java语言描述算法,关键之处给出注释 ```c++ int M_Search(int A[],int B[],int n){ int s1=0,d1=n-1,m1,s2=0,d2=n-1,m2; //分别表示序列A和B的首位数,末位数和中位数 while(s1!=d1||s2!=d2){ m1=(s1+d1)/2; m2=(s2+d2)/2; if(A[m1]==B[m2]) return A[m1]; //满足过程1 if(A[m1]n/2(0\leq p_k < n,1 \leq k \leq m)$,则称x为A的主元素,例如A=(0,5,5,3,5,7,5,5),则5为主元素;又如A=(0,5,5,3,5,1,5,7),则A中没有主元素。假设A中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出A的主元素,若存在主元素,则输出该元素,否则输出-1,要求 1)给出算法设计思想 算法的基本设计思想:从前往后扫描数组元素,标记出一个可能成为主元素的元素num,然后重新计数,确认num是否是主元素 1. 选取候选的主元素,依次扫描所给数组中的每个整数,将第一个遇到的整数Num保存到c中,记录num的出现次数为1;若遇到的下一个整数仍然等于num,则计数加一,否则计数减一;当计数减到0时,将遇到的下一个整数保存到c中,计数重新记为一,开始新一轮计数,即从当前位置开始重复上述过程,直到扫描完全部数组元素 2. 判断c中元素是否是真正的主元素。再次扫描该数组,统计c中元素出现的次数,若等于n/2,则为主元素;否则,序列中不存在主元素 2)采用C或C++或java语言描述算法,关键之处给出注释 ```c++ int Majority(int A[],int n){ int i,c,count=1; //c用来保存候选主元素,count计数 c=A[0]; //设置A[0]为候选主元素 for(i=1;i0) //处理不是候选主元素的情况 count--; else{ //更换候选主元素,重新计数 c=A[i]; count=1; } if(count>0) for(i=count=0;in/2) //确认候选主元素 return c; else //不存在主元素 return -1; } ``` 3)说明算法时间空间复杂度 时间复杂度为$O(n)$,空间复杂度为$O(1)$ 13. 给定一个含$n(n \geq 1)$个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数,例如,数组{-5,3,2,3}中未出现的最小正整数是1;数组{1,2,3}中未出现的最小正整数是4,要求: 1)给出算法设计思想 思路:采用空间换时间的方法,分配一个用于标记的数组B[n],用于记录A中是否出现了1~n中的正整数,B[0]对应正整数1,B[n-1]对应正整数n,初始化B中全部为0,由于A中含有n个整数,因此可能的返回值是1~n+1,当A中n个数恰好为1~n时返回n+1。当数组A中出现了小于等于0或大于n的值时,会导致1~n中出现空余位置,返回结果必然在1~n中,因此对于A中出现了小于等于0或大于n的值,可以不采取任何操作 算法流程:从A[0]开始遍历A,若00&&A[i]<=n) //若A[i]的值介于1~n,则标记数组B B[A[i]-1]=1; for(i=0;i0){ D=abs_(A[i]-B[j])+abs_(B[j]-C[k])+abs_(C[k]-A[i]); //计算D if(Dnext = NULL; scanf("%d",&x); while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=L->next; L->next=s; scanf("%d",&x); } return L; } ``` 读入数据的顺序与生成的链表中的元素的顺序是相反的,每个结点插入的时间为$O(1)$,设单链表长度为n,则总时间复杂度为$O(n)$ #### 采用尾插法建立单链表 该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点 算法: ```c LinkList List_TailInsert(LinkList &L){ //正向建立单链表 int x; L=(LinkList)malloc(sizeof(LNode)); LNode *s,*r=L; //r为表尾指针 scanf("%d",&x); while(x!=9999){ s=(LNode *)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; //r指向新的表尾指针 scanf("%d",&x); } r->next=NULL; //尾结点指针置空 return L; } ``` 读入数据的顺序与生成的链表中的元素的顺序一致,附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同,都为$O(n)$ #### 按序号查找结点值 在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL 算法: ```c LNode *GetElem(LinkList L,int i){ int j=1; //计数,初始为1 LNode *p=L->next; //头结点指针赋给p if(i==0) return L; //若i等于0,则返回头结点 if(i<1) return NULL; //i无效则返回NULL while(p&&jnext; j++; } return p; //返回第i个结点的指针,若i大于表长,则返回NULL } ``` 时间复杂度为$O(n)$ #### 按值查找表结点 从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL 算法: ```c LNode *LocateElem(LinkList L,ElemType e){ LNode *p=L->next; while(p!=NULL&&p->data!=e) //从第一个结点开始查找data域为e的结点 p=p->next; return p; //找到后返回该结点指针,否则返回NULL } ``` #### 插入结点操作 插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点 算法首先调用按序号查找算法GetElem(L,i-1),查找第i-1个结点。假设返回的第i-1个结点为* p,然后令新结点* s的指针域指向* p的后继结点,再令结点* p的指针域指向新插入的结点* s 实现插入结点的代码片段: ```c p=GetElem(L,i-1); s->next=p->next; p->next=s; ``` 上述片段中,指针操作顺序不能颠倒,否则,先执行p->next=s后,指向其原后继的指针就不存在,再执行s->next=p->next时,相当于执行了s->next=s,显然是错误的 主要的时间开销在于查找第i-1个元素,时间复杂度为$O(n)$.若在给定的节点后面插入新结点,则时间复杂度为$O(1)$ #### 对某一结点进行前插操作 前插通常为在某结点的前面插入一个新结点,后插则相反,且单链表插入算法中更常用后插操作 上述算法中,找到插入结点的前驱结点后再执行后插操作即可将前插操作转换为后插操作,前提是从单链表头结点开始顺序查找到其前驱结点,时间复杂度为$O(n)$ 也可设待插入结点为*S, 将 *S插入到到 *P的前面,此时仍然可以将 *S插入到 *P后,将p->data与s->data交换,此时的时间复杂度为$O(1)$ ```c //将 *S插入到到 *P的前面 s->next = p->next; //修改指针域 p->next = s; temp = p->data; //交换数据域 p->data = s->data; s->data = temp; ``` #### 删除结点操作 删除结点操作是江单链表的第i个结点删除。需要先检查删除位置的合法性,后查找表中第i-1个结点,即被删结点的前驱结点,再将其删除 假设*p为找到的被删结点的前驱结点,仅需修改 *p的指针域,即将 *p的指针域next指向 *q的下一结点 ```c p=GetElem(L,i-1); //查找删除位置的前驱结点 q=p->next; //和后继结点交换数据域 p->next=q->next; //将*q结点从链中断开 free(q); //释放后继结点的存储空间 ``` #### 求表长操作 即计算单链表中数据结点的个数,需从第一个结点开始顺序依次访问表中的每个结点,设置一个计算器变量,每访问一次结点则加一,直到访问空结点,算法复杂度为$O(n)$ 单链表长度往往不包括头结点,对于不带头结点和带头结点的链表在求表长时操作存在不同。对于前者,当表空时需要单独处理 ### 双链表 双链表在单链表的结点中增加了一个指向前驱的prior指针,使得其无需像单链表那样只能从头开始依次顺序地向后遍历 结点类型描述: ```c typedef struct DNode{ //定义双链表结点类型 ElemType data; // 数据域 struct DNode *prior,*next; //前驱和后驱结点 }DNode,*DLinklist; ``` #### 双链表的插入操作 在双链表中p所指的结点之后插入结点*s ```c s->next=p->next; //将结点*s插入到结点*p之后 p->next->prior=s; s->prior=p; p->next=s; ``` 上述代码顺序并非唯一,但也不是任意的,第一二步需保证在第四步之前,当值丢失*p的后继结点的指针 #### 双链表的删除操作 删除双链表中结点*p的后继结点 *q ```c p->next=q->next; q->next->prior=p; free(q); ``` ### 循环链表 #### 循环单链表 循环单链表与单链表的区别在表中最后一个结点的指针不是NULL,而是指向头结点,从而整个链表形成一个环 循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件为头结点的指针是否等于头指针 循环单链表中插入,删除与单链表一致,不同在于表尾操作时需要让单链表继续保持循环的性质。当然由于循环单链表往往认为是一个环,任何一个位置上的插入和删除操作都是等价,无需判断是否为表尾 相比于单链表,循环单链表能够从表中任意一个结点开始遍历整个链表,有时对单链表常做的操作是在表头和表尾进行,此时对循环单链表不设头指针,仅设尾指针能够有更高的效率,原因在于,若设的是头指针,对表尾进行操作需要$O(n)$的时间复杂度,若设的是尾指针r,r->即为头指针,对表头与表尾进行操作都只要$O(1)$的时间复杂度 #### 循环双链表 在循环双链表中,头结点的prior指针还要指向表尾结点 例如在循环双链表L中,某结点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior域和next域都等于L ### 静态链表 静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与先前的链表中的指针不同在于此处的指针是结点的相对地址(数组下标),又称游标。和顺序表一致,静态链表也需要分配一块连续的内存空间 结构类型描述: ```c #define MaxSize 50 typedef struct { ElemType data; int next;//下一个元素的数组下标 } SLinkList[MaxSize]; ``` 静态链表以next == -1作为结束的标志。总体而言,静态链表不如单链表使用起来方便,但在一些不支持指针的高级语言中,其为一种巧妙的设计方法 ## 顺序表和链表的比较 ### 存取(读写)方式 顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置上执行存或取得操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次 ### 逻辑结构与物理结构 采用顺序存储是,逻辑上相邻的元素,对应的物理存储位置也相邻。采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的 ### 查找、插入和删除操作 对于按值查找,顺序表无序时,两者的时间复杂度均为$O(n)$;顺序表有序时,可采用折半查找,此时的时间复杂度为$O(\log_2 n)$ 对于按序号查找,顺序表支持随机访问,时间复杂度仅为$O(1)$,而链表的平均时间复杂度为$O(n)$。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大 ### 空间分配 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后不大量闲置;预先分配过小则易发生溢出 动态分配情形下,虽然可以扩充存储空间,但需要移动大量元素,导致操作效率降低,若内存中没有更大块的连续存储空间,则会分配失败,链式存储的存储空间则只在需要时申请,只要空间足够就能够申请,操作灵活高效 ### 存储结构的选取考虑 #### 基于存储考虑 难以估计线性表长度或存储规模时,不宜采用顺序表; 链表不用事先估计存储规模,但链表的存储密度低(低于1) #### 基于运算的考虑 在顺序表中按序号访问$a_j$的时间复杂度为$O(1)$,而链表中按序号访问的时间复杂度为$O(n)$,因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表 顺序表中插入,删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,此开销不可忽视;在链表中进行插入、删除操作时,虽然也要找插入位置,但主要进行的是比较操作,可见后者优于前者 #### 基于环境的考虑 顺序表易于实现,任何高级语言中都有数组类型;链表的操作是基于指针的,相对来讲,前者实现较为简单 两者各有优缺点,通常较稳定的线性表选择顺序存储,而频繁进行插入、删除操作的线性表宜使用链式存储 ### 一些练习 1. 设计一个递归算法,删除不带头结点的单链表L中所有值为x的结点 设计f(L,x)的功能是删除以L为收结点指针的单链表中所有值等于x的结点,显然有f(L->next,x)的功能是删除以L->next为首结点指针的单链表中所有值等于x的结点。由此,可以推出递归模型如下。 终止条件: f(L,x) = 不做任何事情; 若L为空表 递归主体: f(L,x) = 删除*L结点;f(L->next,x); 若L->data == x f(L,x) = f(L->next,x); 其他情况 代码如下: ```c void Del_X_3(Linklist &L,ElemType x){ //递归实现在单链表L中删除值为x的结点 LNode *p; //p指向待删除结点 if(L==NULL) //递归出口 return; if(L->data==x){ //若L所指结点的值为x p=L; //删除*L,并让L指向下一结点 L=L->next; free(p); Del_X_3(L,x); //递归调用 } else //若L所指结点的值不为x Del_X_3(L->next,x);//递归调用 } ``` 算法需要调用一个递归工作栈,深度为O(n),时间复杂度为O(n)。由于L为引用,是直接对原链表进行操作,因而不会发生断链 2. 试L为带头结点的单链表,编写算法实现从尾到头反向输出每个结点的值 算法思想:每当访问一个结点时,先递归输出它后面的结点,再输出该结点自身,这样链表就反向输出了 代码: ```c void R_Print(LinkList L){ //从尾到头输出单链表L中每个结点的值 if(L->next!=NULL){ R_Print(L->next); //递归 }//if if(L!=NULL){ print(L->data); } } void R_Ignore_Head(LinkList L){ if(L!=NULL){ R_Print(L->next); } } ``` 3. 有一个带头结点的单链表L,试设计一个算法使其元素递增有序 算法思想: 采用直接插入排序算法的思想,先构成只含一个数据结点的有序单链表,然后一次扫描单链表中剩下的结点*p(直到p==NULL为止),在有序表中通过比较查找插入 *p的前驱结点 *pre,然后将 *p插入到 *pre之后 代码如下: ```c void Sort(LinkList &L){ //本算法实现将单链表L的结点重排,使其递增有序 LNode *p=L->next,*pre; LNode *r=p->next; //r保持*p后继结点指针,保证不断连 p->next = NULL; //构造只含一个数据结点的有序表 p=r; while(p!=NULL){ r=p->next; //以保存*p的后继结点指针 pre=L; while(pre->next!=NULL&&pre->next->datadata){ pre = pre->next; //在有序表中查找插入*p的前驱结点*pre } p->next = pre->next; //将*p插入到*pre之后 pre->next = p; p=r; //扫描原单链表中剩下的结点 } } ``` 该算法的时间复杂度为$O(n^2)$,为达到最佳的时间性能,可将链表的数据复制到数组中,再采用时间复杂度为$O(n\log_2 n)$的排序算法进行排序,然后将数组元素依次插入链表中,此时的时间复杂度为$O(n\log_2 n)$,显然这是以空间换时间的策略 4. 已知一个带有表头结点的单链表,结点结构为 | data | link | | :---: | :---: | 假设该链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表中倒数第k个位置上的结点(k为整数)。若查找成功,算法输出该结点的data域的值,并返回1;否则,只返回0,要求: 1) 描述算法的基本设计思想 问题关键在于设计一个尽可能高效的算法,通过链表的一次遍历,找到倒数第k个结点的位置 算法的基本设计思想: 定义两个指针变量p和q,初始时均指向头结点的下一个结点(链表的第一个结点),p指针沿链表移动,当p指针移动到第k个结点时,q指针开始与p指针同步移动;当p指针移动到最后一个结点时,q指针所指示结点为倒数第k个结点。以上过程对链表仅进行一遍扫描 2) 描述算法的详细实现步骤 1. count = 0,p和q指向链表表头结点的下一个结点 2. 若p为空,转向步骤5 3. 若count等于k,则q指向下一个结点,否则,count=count+1. 4. p指向下一个结点,转向步骤2 5. 若count等于k,则查找成功,输出该结点的data域的值,返回1,否则,说明k值超过了线性表长度,查找失败,返回0 6. 算法结束 3) 根据设计思想和实现步骤,采用程序设计语言描述算法,关键之处请给出简要注释 算法实现: ```c typedef int ElemType //链表数据的类型定义 typedef struct LNode{ ElemType data; //结点数据 struct LNode *link;//结点链接指针 }LNode,*LinkList; //链表结点的结构定义 int Search_k(LinkList list,int k){ //查找链表list倒数第k个结点,并输出该结点data域的值 LNode *p=list->link,*q=list->link;//指针p,q指示第一个结点 int count=0; while(p!=NULL){ // 遍历链表直到最后一个结点 if(countlink; } p=p->link; //之后让p,q同步移动 } if(countdata); //查找成功,打印并返回1 return 1; } } ``` 5. 设计一个算法完成以下功能:判断一个链表是否有环,如果有,找出环的入口点并返回,否则返回NULL 算法的基本设计思想: 设置快慢两个指针分别为fast和slow,初始时都指向链表头head。slow每次走一步,即slow=slow->next;fast每次走两步,即fast=fast->next->next。由于fast比slow走得快,如果有环,那么fast一定会先进入,而slow后进入环,当两个指针都进入环后,经过若干操作后两个指针定能在环上相遇,从而判断一个链表是否存在环 当slow刚进入环时,fast早已进入环。因为fast每次比slow多走一步,且fast与slow 的距离小于环的长度,所以fast与slow相遇时,slow所走的距离不超过环的长度 设头结点到环的入口点的距离为a,环的入口点沿着环的方向到相遇点的距离为x,环长为r,相遇时fast绕过了n圈,则有2(a+x)=a+n * r+x,即a=n * r-x。显然从头结点到环的入口点的距离等于n被的环长减去环的入口点到相遇点的距离。因此可设置两个指针,一个指向head,一个指向相遇点,两个指针同步移动(一次走一步),相遇点即为环的入口点 代码实现: ```c LNode* FindLoopStart(LNode *head){ LNode *fast=head,*slow=head;//设置快慢两个指针 while(slow!=NULL&&fats->next!=NULL){ slow=slow->next; //每次走一步 fast=fast->next-next;//每次走两步 if(slow==fast){ // 相遇 break; } } if(slow==NULL||fast->next==NULL){ return NULL; //没有环,返回NULL } LNode *p1=head,*p2=slow; //分别指向开始点,相遇点 while(p1!=p2){ p1=p1->next; p2=p2->next; } return p1; //返回入口点 } ``` 6. 设线性表$L=(a_1,a_2,a_3,\cdots,a_{n-2},a_{n-1},a_{n})$采用带头结点的单链表保存,链表中的结点定义如下: ```c typedef struct node { int data; struct node*next; }NODE; ``` 请设计一个空间复杂度为$O(1)$且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表$L_1=(a_1,a_n,a_2,a_{n-1},a_3,a_{n-2},\cdots)$。要求: 1) 描述算法的基本设计思想和详细实现步骤 观察比较$L$和$L_1$可知,后者由前者摘取一个元素,再摘取倒数第一个元素,依次合并而成 为了方便链表后半段取元素,需要先将$L$的后半段原地逆置(题目要求空间复杂度为$O(n)$因而不能借助栈来逆置),否则每取最后一个结点都需要遍历一次链表 1. 先找出链表L的中间结点,为此设置两个指针p和q,指针p每次走一步,指针q每次走两步,当指针q到达链尾时,指针p正好在链表的中间结点 2. 然后将L的后半段结点原地逆置 3. 从单链表前后两段中依次各取一个结点,按要求重排 2) 根据设计思想和实现步骤,采用程序设计预言描述算法,关键之处请给出简要注释 ```c void change_list(NODE*h){ NODE *p,*q,*r,*s; p=q=h; while(q->next!=NULL){ //寻找中间结点 p=p->next; //p走一步 q=q->next; if(q->next!=NULL){ q=q->next; //q走两步 } q=p->next; //p所指结点为中间结点,q为后半段链表的首结点 p->next=NULL; while(q!=NULL){ //逆置链表后半段 r=q->next; q->next=p->next; p->next=q; q=r; } } s=h->next; //s指向前半段的第一个数据结点,插入点 q=p->next; //q指向后半段的第一个数据结点 p->next=NULL; while(q!=NULL){ //将链表后半段的结点插入到指定位置 r=q->next; //r指向后半段的下一个结点 q->next=s->next; //将q所指结点插入到s所指结点之后 s->next=q; s=q->next; //s指向前半段的额下一个插入点 q=r; } } ``` 3) 计算时间复杂度 第一步中找中间结点的时间复杂度为$O(n)$,第二步逆置的时间复杂度为$O(n)$,第三部合并链表的时间复杂度为$O(n)$,因此该算法的时间复杂度为$O(n)$