《王道24数据结构》课后应用题——第二章

文章目录

  • 第二章
  • 【2.2】
    • 01、
    • 02、
    • ==03、==
    • 04、
    • ==05、==
    • ==06、==
    • 07、
    • 08、
    • 09、
    • 10、
    • ==11、==
    • 12、
    • ==13、==
    • 14、
  • 【2.3】
    • ==01、==
    • ==02、==
    • ==03、==
    • ==04、==
    • 05、
    • ==06、==
    • 07、
    • ==08、==
    • 09、
    • 10、
    • 11、

编程题须知:

  • 编程题不限语言。也可以用标准库函数,如C++中等。
  • 编程题一定要写注释。 注释能够引导老师去理解你的代码。如果单纯的写代码,没有一个老师会认真看,即使你的代码很漂亮,很巧妙。
  • 编程题可以以这样一个格式去写:
    • 第一部分:写一段话来表述你的算法思想。
    • 第二部分:写你的具体代码,并在关键地方,需要的地方,给出尽可能详细的注释。
    • 第三部分(时间够多):可以分析分析你的代码还有哪里不足,如何改进等
  • 命名要规范:推荐驼峰命名法命名:int firstValue 替换 int i,j,k;

第二章

【2.2】

01、

  • 从顺序表中删除具有最小值的元素(假设唯一) 并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空,则显示出错信息并退出运行。

算法思想:

  用数组存放顺序表,对数组元素从头到尾依次遍历,找到最小值元素值min,下标为minIndex,将之与数组最后一个元素值互换,然后删去数组最后一个元素即可。

算法 DeleteMinElem(A, n)
/* 找到数组A中最小元素,将其与数组最后一个元素互换并删除 */
D1.[表为空?]
IF n=0 THEN RETURN -1.	//数组长度为0,则报错,返回-1
D2.[初始化]
min←A[0].
minIndex←0.
D3.[遍历找最小元素]
FOR i←1 TO n-1 DO
	IF A[i]<min THEN
		min←A[i].
		minIndex←i.
D4.[与尾元素互换]
A[minIndex]A[n-1].		//最后一个元素值赋至被删除元素处
n←n-1.					//表长减1,从而删去表尾元素
D5.[返回被删除元素值]
RETURN min.				//函数返回被删除元素的值

02、

  • 设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为 O(1)。

算法思想:

  对于一个包含 n n n个元素的顺序表L,将其中第 i i i个元素与第 n − i + 1 n-i+1 ni+1个元素的值进行交换即可,其中 1 ≤ i ≤ ⌊ n / 2 ⌋ 1≤i≤\lfloor n/2\rfloor 1in/2

算法 Reverse(A, n)
/* 逆置数组,将数组中第i个元素与第n-i+1个元素进行交换 */
FOR i←1 TO n/2 DO
	t←A[i].
    A[i]A[n-i+1].
    A[n-i+1]←t.
RETURN A.

03、

  • 对长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。

算法思想:

  顺序表L存放在数组A中,从前往后依次遍历该数组,若在位置i处遇到值为x的元素,则将其与表尾元素互换,并使表长减1,之后,继续从当前位置i向后遍历,做相同操作直至遍历完毕。

算法 DeleteElems(A, n, x)
/* 遍历数组A,若遇到值为x的元素,则与数组尾部元素互换,并使表长减1 */
FOR i←0 TO n-1 DO
	IF A[i]=x THEN
    	A[i]A[n-1].	//表尾元素值覆盖此处
        n←n-1.			//表长减1
		i←i-1.			//由于表尾元素值有可能也为x,所以下一轮循环仍从i处开始
RETURN A.

我这个方法可以是可以,但是可以使用一个更加精炼的表达方式,如下:

设头、尾两个指针(i=1,j=n),从两端向中间移动,在遇到最左端值为x的元素时,将最右端非x的元素移动至左端值为x的元素位置。直至两指针相遇为止。

但是这种做法会改变原表中元素的相对位置。

不改变原表元素相对位置的方法,如下:

解法1

  用k记录顺序表L中不等于x的元素个数(即需要保存的元素个数),扫描时将不等于x的元素移动到下标k的位置,并更新k值。扫描结束后修改L的长度。

算法 DeleteElem_x(L, x)
/* 本算法实现删除顺序表L中所有值为x的元素 */
D1.[初始化]
k←0.	//k记录不等于x的元素个数
D2.[遍历]
FOR i←0 TO L.length-1 DO (
	IF L.data[i]≠x THEN (
		L.data[k]L.data[i].
		k←k+1. )	//不等于x的元素个数增1
)
L.length←k.		//更新顺序表L的长度,即删除所有值为x的元素

解法2

  用k记录顺序表L中等于x的元素个数,边扫描L边 统计k,并将不等于x的元素前移k个位置。扫描结束后修改L的长度。

算法 DeleteElem_x(L, x)
D1.[初始化]
k←0.		//k记录值为x的元素个数
i←0.		//i用于遍历
D2.[遍历]
WHILE i<L.length DO (
	IF L.data[i]=x THEN k←k+1.
	ELSE L.data[i-k]L.data[i].	//当前元素前移k个位置
    i←i+1. )
L.length←L.length-k.	//修改顺序表L的长度

04、

  • 从有序顺序表中删除其值在给定值s与t之间(要求s

算法思想:

  从前向后依次遍历该有序顺序表,找到值大于等于s的第一个元素,其位置为pos1(第一个被删除元素的位置),再找到值大于t的第一个元素,其位置为pos2(最后一个被删除元素的下一个位置)。之后,从pos2开始到表尾,所有元素均前移pos2-pos1个位置,最后修改L的表长即可。

05、

  • 从顺序表中删除其值在给定值s与t之间(包含s和t,要求 s

算法思想:

  从前向后依次遍历该顺序表L,对每个元素的值进行判断,若某处元素值位于s和t之间,则删除之。

该算法过于暴力,较优的算法思想如下:

(和第3题删除所有等于x的元素,思想很类似)

算法思想:

  从前向后扫描顺序表L,用k记录下元素值在st之间元素的个数(初始时k=0)。对于当前扫描的元素,若其值不在s到t之间,则前移k个位置;否则k++。该算法时间效率为 O ( n ) O(n) O(n)

评价:这个题和第3题思想本质一致,只不过第三题的k是记录值等于x的元素个数,而此处k是记录值介于s、t之间的元素个数。

06、

  • 从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。

算法思想:

  从前向后扫描顺序表L,用k记录下当前遇到的重复元素个数(初始时k=0)。对于当前扫描的元素,若其值与前驱元素不相等,则前移k个位置;否则k++。

(上面是我自己想的,好像是行得通的)

下面是王道的答案:

算法思想:

  由于是有序顺序表,所以可以用类似于直接插入排序的思想。初始时,将第一个元素视为一个非重复的有序表,之后依次判断后面的元素是否与前面非重复有序表的最后一个元素相同;若相同,则跳过、继续向后遍历;若不同,则插入前面非重复有序表的尾端。

【扩展】

  • 如果将本题的有序表改为无序表,你能给出一个时间复杂度为 O ( n ) O(n) O(n)的方法吗?

**思想:**使用散列表。

07、

  • 将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。

算法思想:

  另外创建一个新的顺序表L,对于这两个顺序表A与B,设置两个遍历指针ij从其表头开始依次遍历,对于两个指针所指向的元素,将较小的那个元素插入新表L的尾部。若某指针已经遍历至当前表尾,则另一个表中剩下元素直接全部插入。

王道答案对此给的描述:(一个意思)

  首先,按顺序不断取下两个顺序表表头较小的结点存入新的顺序表中。然后,看哪个表还有剩余,将剩下的部分加到新的顺序表后面。

08、

  • 已知在一维数组A[m+n]中依次存放两个线性表(a1,a2,...am)(b1,b2,...bn)。编写一个函数,将数组中两个顺序表的位置互换,即将(b1,b2,...,bn)放在(a1,a2,...,am)前面。

算法思想:

  先分别将子表(a1,...am)逆置、子表(b1,...bn)逆置,最后将整个数组A逆置即可。

或者改一下顺序也行:

  先将整个数组A逆置,然后再将其中的两个子表逆置。

09、

  • 线性表(a1,a2,a3,...,an)中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素仍递增有序。

算法思想:

  使用二分查找查询表中是否存在值为x的元素。若查找成功,则将其与其后继元素互换;若查找失败,则在折半查找最后的 h i g h high high指针位置处插入该元素。

10、

[2010 统考真题]

  • 设将 n(n>1)个整数存放到一维数组R中。设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p(O ( X 0 , X 1 , . . . X n − 1 ) (X_0,X_1,...X_{n-1}) (X0,X1,...Xn1)变换为 ( X p , X p + 1 , . . . , X n − 1 , X 0 , X 1 , . . . , X p − 1 ) (X_p,X_{p+1},...,X_{n-1},X_0,X_1,...,X_{p-1}) (Xp,Xp+1,...,Xn1,X0,X1,...,Xp1)。要求:

    1)给出算法的基本设计思想。

    2)根据设计思想,采用 C或 C++或 Java 语言描述算法,关键之处给出注释。

    3)说明你所设计算法的时间复杂度和空间复杂度

这个算法和第8题一模一样。

对于数组R,先将其中前p个元素逆置,再将其中后n-p个元素逆置,最后将整个数组逆置即可。

11、

[2011 统考真题]

  • 一个长度为 L(L≥1)的升序序列S,处在第 ⌈ L / 2 ⌉ \lceil L/2\rceil L/2个位置的数称为S的中位数。例如,若序列 S 1 = ( 11 , 13 , 15 , 17 , 19 ) S_1=(11,13,15,17,19) S1=(11,13,15,17,19),则 S 1 S_1 S1的中位数是15,两个序列的中位数是含它们所有元素的升序序列的中位数。例如,若 S 2 = ( 2 , 4 , 6 , 8 , 20 ) S_2=(2,4,6,8,20) S2=(2,4,6,8,20),则 S 1 S_1 S1 S 2 S_2 S2的中位数是11。现在有两个等长升序序列A和 B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。要求:

    1)给出算法的基本设计思想。

    2)根据设计思想,采用C或C++或Java 语言描述算法,关键之处给出注释。

    3)说明你所设计算法的时间复杂度和空间复杂度。

个人思路(比较常规):

  先将这两个升序序列合并为一个大的升序序列,然后取中位数即可。思想和第7题一样。

答案思路,如下:

算法思想:

  分别求两个升序序列 A 、 B A、B AB的中位数,设为ab,求序列 A 、 B A、B AB的中位数过程如下:

  ①若a=b,则ab即为所求中位数,算法结束。

  ②若a,则舍弃序列A中较小的一半,同时舍弃B中较大的一半。要求两次舍弃的长度相等。

  ③若a>b,则舍弃序列A中较大的一半,同时舍弃B中较小的一半。要求两次舍弃的长度相等。

  在舍弃后留下的两个升序序列中,重复上述过程①②③,直到两个序列中均只含有一个元素为止,较小者即为所求的中位数。

代码:

int M_Search(int A[], int B[], int n) {
    int s1=0, d1=n-1;	//序列A的首位、末位
    int m1;		//序列A的中位数
    int s2=0, s2=n-1;	//序列B的首位、末位
    int m2;		//序列B的中位数
    
    while(s1!=d1 || s2!=d2) {	//两个序列中含有元素个数大于1时执行循环
        m1 = (s1+d1)/2;
        m2 = (s2+d2)/2;
        if(A[m1]==B[m2]) return A[m1];	//满足条件①
        if(A[m1]<B[m2]) {
            if((s1+d1)%2==0) {	//若序列长度为偶数
            	s1 = m1+1;		//舍弃A中间点处以及中间点前面那半段
            	d2 = m2;		//舍弃B中间点后面那半段
            }
            else {		//若序列长度为奇数
                s1 = m1;		//舍弃A中间点前面那半段
                d2 = m2;		//舍弃B中间点后面那半段
            }
        }
        else {
            if((s1+d1)%2==0) {	//若序列长度为偶数
            	d1 = m1;		//舍弃A中间点后面那半段
            	s2 = m2+1;		//舍弃B中间点处以及中间点前面那半段
            }
            else {		//若序列长度为奇数
                d1 = m1;		//舍弃A中间点后那半段
                s2 = m2;		//舍弃B中间点前面那半段
            }
        }
    }
    return A[s1]<B[s2]?A[s1]:B[s2];
}

效率分析:

  该算法的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),空间复杂度为 O ( 1 ) O(1) O(1)

12、

[2013 统考真题]

  • 已知一个整数序列 A = ( a 0 , a 1 , . . . , a n − 1 ) A=(a_0,a_1,...,a_{n-1}) A=(a0,a1,...,an1),其中 0 ≤ a i < n 0≤a_i0ai<n 0 ≤ i < n 0≤i0i<n)。若存在 a p 1 = a p 2 = . . . = a p m = x a_{p1}=a_{p2}=...=a_{pm}=x ap1=ap2=...=apm=x m > n / 2 m>n/2 m>n/2 0 ≤ p k < n , 1 ≤ k ≤ m 0≤p_k0pk<n,1km),则称x为A的主元素。例如 A = ( 0 , 5 , 5 , 3 , 5 , 7 , 5 , 5 ) A=(0,5,5,3,5,7,5,5) A=(0,5,5,3,5,7,5,5),则5为主元素;又如 A = ( 0 , 5 , 5 , 3 , 5 , 1 , 5 , 7 ) A=(0,5,5,3,5,1,5,7) A=(0,5,5,3,5,1,5,7),则A中没有主元素。假设 A中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出A的主元素。若存在主元素,则输出该元素;否则输出-1。要求:

    1)给出算法的基本设计思想。

    2)根据设计思想,采用 C或 C++或 Java 语言描述算法,关键之处给出注释。

    3)说明你所设计算法的时间复杂度和空间复杂度。

把题目用大白话说一下,就是,一个序列有n个元素,且每个元素的值位于 [ 0 , n ) [0,n) [0,n)之间。如果某个元素x的出现次数大于n/2了,那么这个x就是这个序列的主元素。(根据这个定义来看,显然,一个序列的主元素如果有,则一定是唯一的)。

算法思想:

  由于序列中元素的数值是0≤x的,所以可以设置一个大小为n的数组,依次存放这个序列中各个数字的出现次数。具体方法是,对序列进行遍历,遇到某个数字k,就将存放元素次数的数组对应位置处加1(time[k]++)即可。最后访问存放次数的数组,找出次数最大的那个元素即可。

  时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

答案我没看懂,想看的话自己去翻王道书。

答案给的算法,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

算法思想:

  也可以直接对原始序列进行排序,最好的排序算法时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),即使用了较差的排序算法 O ( n 2 ) O(n^2) O(n2)也无妨。排好序之后便可以很轻松的找到主元素。

【注意】

  注意一个问题,考试的时候能写完整、写正确、写得快,是首要的。如果想不到最好的算法,写个普通的,让他扣个1、2、3分也无所谓。

13、

[2018 统考真题]

  • 给定一个含 n(n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组 { − 5 , 3 , 2 , 3 } \{-5,3,2,3\} {5,3,2,3}中未出现的最小正整数是1;数组 { 1 , 2 , 3 } \{1,2,3\} {1,2,3}中未出现的最小正整数是4。要求:

    1)给出算法的基本设计思想。

    2)根据设计思想,采用 C或 C++语言描述算法,关键之处给出注释。

    3)说明你所设计算法的时间复杂度和空间复杂度。

算法思想:

  对数组直接进行升序排序。之后,对排好序的升序数组,从前往后找到第一个大于0的元素,便可得知未出现的最小正整数是几了。

答案的思想如下:

算法思想:

  要求在时间上尽可能高效,因此采用空间换时间的办法。

  分配一个用于标记的数组 B [ n ] B[n] B[n],用来记录 A A A中是否出现了1~n中的正整数,B[0]对应正整数1B[n-1]对应正整数n,初始化数组B中元素全部为0。

  由于 A A A中含有 n n n个整数,因此可能得到的最小正整数的值是1~n+1

这一句话是理解的关键。我看似没有束缚整数的取值范围,但是我根据整数的个数,就能推知最小正整数的范围。

例如:随便给5个数,要么是1、2、3、4、5,没出现的最小正整数为6,或者你给出1、2、3、100、200,那没出现的最小正整数为4,或者你给出100、200、300、400、500,那没出现的最小正整数为1

总之,随便给5个数字,没出现的最小正整数,它就不可能是个7。或者你反过来想,如果没出现的最小正整数是7了,那就说明1、2、3、4、5、6必定全部出现了,而显然仅凭5个数字是无法做到这一点的。

所以,随便给出 n n n个整数,那么没有出现的最小正整数的范围就是1~n+1

  当 A A A n n n个数恰好为1~n的时候,未出现的最小正整数为n+1。当数组 A A A中出现了≤0或者>n的值时,会导致1~n中出现空余位置,那么未出现的最小正整数必然会取在1~n中。因此,对于 A A A中那些≤0或者>n的值,不采取任何操作。

  由以上分析,可以得出算法流程:

  从 A [ 0 ] A[0] A[0]开始遍历 A A A,若 0 < A [ i ] < n 00<A[i]<n,则令 B [ A [ i ] − 1 ] = 1 ; B[A[i]-1]=1; B[A[i]1]=1;否则不作操作。

  对 A A A遍历结束后,开始遍历数组 B B B,若能查找到第一个满足 B [ i ] = = 0 B[i]==0 B[i]==0的下标 i i i,则返回i+1即为结果,此时也说明 A A A中未出现的最小正整数在1~n之间;若 B [ i ] B[i] B[i]遍历完毕,全部不为0,则同样返回i+1(实际上就是n+1了),此时说明 A A A中未出现的最小正整数是n+1

代码:

int findMissMin(int A[], int n) {
    int i;
    int *B;		//标记数组
    B = (int *)malloc(n * sizeof(int));		//分配空间
    memset(B, 0, n*sizeof(int));	//赋初值为0
    for(i=0; i<n; i++) {
        if(A[i]>0 && A[i]<=n)	//若A[i]的值介于1~n,则标记数组B
            B[A[i]-1]=1;
    }
    for(i=0; i<n; i++) {	//扫描数组B的过程中,找到目标值
        if(B[i]==0)
            break;
    }
    return i+1;	//返回结果
}

效率分析:

  遍历 A A A一次,之后遍历 B B B一次,因此时间复杂度为 O ( n ) O(n) O(n)。由于额外分配了数组 B [ n ] B[n] B[n],因此空间复杂度为 O ( n ) O(n) O(n)

14、

[2020统考真题]

  • 定义三元组 ( a , b , c ) (a,b,c) (a,b,c)(a,b,c均为整数)的距离 D = ∣ a − b ∣ + ∣ b − c ∣ + ∣ c − a ∣ D=|a-b|+|b-c|+|c-a| D=ab+bc+ca。给定3个非空整数集合 S 1 、 S 2 和 S 3 S_1、S_2和S_3 S1S2S3,按升序分别存储在3个数组中。请设计一个尽可能高效的算法,计算并输出所有可能的三元组 ( a , b , c ) (a,b,c) (a,b,c) a ∈ S 1 , b ∈ S 2 , c ∈ S 3 a∈S_1,b∈S_2,c∈S_3 aS1,bS2,cS3)中的最小距离。例如 S 1 = { − 1 , 0 , 9 } S_1=\{-1,0,9\} S1={1,0,9} S 2 = { − 25 , − 10 , 10 , 11 } S_2=\{-25,-10,10,11\} S2={25,10,10,11} S 3 = { 2 , 9 , 17 , 30 , 41 } S_3=\{2,9,17,30,41\} S3={2,9,17,30,41},则最小距离为2,相应的三元组为 ( 9 , 10 , 9 ) (9,10,9) (9,10,9)。要求:

    1)给出算法的基本设计思想。

    2)根据设计思想,采用C语言或 C++语言描述算法,关键之处给出注释。

    3)说明你所设计算法的时间复杂度和空间复杂度。

暴力法:

S 1 、 S 2 、 S 3 S_1、S_2、S_3 S1S2S3所形成三元组的所有排列组合 ( a 、 b 、 c ) (a、b、c) (abc)全部暴力穷举,最后得到最小的 D D D即可。

自己去看答案吧。

【2.3】

01、

  • 设计一个递归算法,删除不带头结点的单链表L中所有值为x的结点。

算法思想:

递归的不太清楚怎么写,如果不要求递归的话,很好写,就从头指针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); //其他情况。

代码:

void DeleteElem_x(Linklist &L, int x) {
    LNode *p;		//p为临时指针,指向待删除结点
    if(L==NULL)		//递归出口
        return;
    if(L->data==x) {	//若L所指结点的值为x
        p=L;  L=L->next;  free(p);	//删除*L,并让L指向下一结点
        DeleteElem_x(L, x);	//递归调用
    }
    else {
        DeleteElem_x(L->next, x);	//递归调用
    }
}

时间复杂度:

  算法借助一个递归工作栈,深度为 O ( n ) O(n) O(n),时间复杂度为 O ( n ) O(n) O(n)

02、

  • 在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的结点不唯一,试编写算法以实现上述操作。

方法一:从头结点开始往后遍历,遇到x就删除。

方法二:使用第1题中的递归算法。

还有别的方法?看下答案。哦。

方法三:将当前单链表L视为一个空表。遇到不是x的结点,依次把它们尾插到L上。遇到是x的结点,将其释放。(有点类似于直接插入排序的思想)

解法1:

  从头至尾扫描单链表,遇到结点值为x的则删除该结点。为了便于这一操作,可以设置两个指针,一个p指针用于扫描单链表,另外附设一个pre指针指向p的前驱。这样,当p所指结点的值为x时,就可以操作pre、p指针对其进行删除,直到遍历完毕。

void DeleteElem_x(Linklist &L, int x) {
    LNode *p = L->next;		//p指针从头至尾遍历单链表
    LNode *pre = L;			//pre指针一直指向p指针的前驱
    LNode *q;		//临时指针q,用于删除结点
    while(p!=NULL) {
        if(p->data == x) {
            q=p;		//q指向要删除的结点
            p = p->next;
            pre->next = p;	//至此,q结点就被断开了
            free(q);	//释放*q结点空间
        }
        else {
            pre = p;
            p = p->next;		//结点不需要删除,两个指针后移1位即可
        }
    }
}

**注意:**该算法是在无序单链表中删除满足某一条件的所有结点。此题中该条件为结点的值等于x。实际上,这个条件可以是任意的,只需修改if里的判定条件即可。

解法2:

  采用尾插法建立单链表。用p指针扫描L的所有结点,当其值不为x时,将其链接到L后,否则将其释放。

void DeleteElem_x(Linklist &L, int x) {
    LNode *p = L->next;		//p对原表进行扫描
    LNode *r = L;			//r指向L的表尾
    LNode *q;
    while(p!=NULL) {
        if(p->data != x) {		//*p结点值不为x时将其链接到L尾部
            r->next = p;
            r = p;				//将p结点尾插,并更新尾指针
            p = p->next;		//p继续扫描
        }
        else {		//*p结点值为x时将其释放
            q = p;
            p = p->next;
            free(q);
        }
    }
    r->next = NULL;		//全部插入结束之后,设尾结点next指针为NULL
}

03、

  • 设L为带头结点的单链表,编写算法实现从尾到头反向输出每个结点的值。

跟第2题的思想有些类似吧。不要忘了单链表只能从前往后遍历,那我们就从前往后遍历;然后思考一下遍历的过程中做什么才能符合题意。

算法思想:

  从前往后遍历单链表,对每个结点,都将其头插至L,即可实现单链表L的逆置。

算法思想:

  也可以借助一个栈,从前往后依次遍历单链表结点并将其入栈。遍历完整个链表后,再将栈中元素依次输出即可。

  既然能用栈的思想解决,那么自然就可以使用递归来实现。每当访问一个结点时,先递归输出它后面的结点,再输出该结点自身,这样链表就能递归地反向输出了。

void ReversePrint(Linklist L) {
    if(L->next != NULL)
        ReversePrint(L->next);		//递归
    if(L!=NULL)
        print(L->data);
}
void ReversePrint_IgnoreHead(Linklist L) {		//L是带头结点的单链表,不输出头结点
    if(L->next!=NULL)
        ReversePrint(L->next);
}

04、

  • 试编写在带头结点的单链表L中删除一个最小值结点的高效算法(假设最小值结点是唯一的)。

那不就很简单么。从前往后遍历,找到最小值min所在的结点,用一个指针保存它。遍历完整个表之后把它删掉。

算法思想:

  用p从头至尾扫描单链表,pre指向p结点的前驱,用minp保存值最小的结点指针(初值为p),minpre指向minp结点的前驱(初值为pre)。

  一边扫描,一边比较,若p->data < minp->data,则将p,pre分别赋值给minp,minpre。当p扫描完毕整个单链表后,minp指向最小值结点,minpre指向最小值结点的前驱结点。此时,将minp所指向结点删除即可。

LinkList DeleteMin(Linklist &L) {
    LNode *pre = L;
    LNode *p = pre->next;	//p为工作指针,pre指向其前驱
    LNode *minpre = pre;
    LNode *minp = p;		//负责保存最小值结点及其前驱
    while(p!=NULL) {
        if(p->data < minp->data) {	//找到比当前最小值结点minp更小的结点
            minp = p;
            minpre = pre;
        }
        pre = p;  p = p->next;	//继续扫描下一个结点
    }
    minpre->next = minp->next;	//删除最小值结点
    free(minp);
    return L;
}

效率分析

  该算法只需进行一次从头至尾扫描单链表,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

注意:本题为带头结点的单链表。自行思考一下,若为不带头结点的单链表,则在细节表达上哪些地方会有所不同。

05、

  • 试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间复杂度为O(1)。

从头至尾依次遍历单链表,对每个结点,都将其头插至头结点L后。

  将头结点L视为一个“空表”,然后对于单链表从头至尾进行遍历,对每个结点,将其依次头插至头结点L对应的表中,相当于一个头插法建立单链表的过程。最终即可实现链表的逆置。

LinkList Reverse_L(LinkList L) {
    LNode *p;	//p为工作指针,负责遍历单链表
    LNode *r;	//r为辅助指针,对p的后继进行暂存,从而在头插之后不影响继续遍历
    p = L->next;	//p的初始值为头结点之后的第一个表结点
    L->next = NULL;	//将表L视为一个空表
    while(p!=NULL) {		//开始头插建表
        r = p->next;	//暂存p的后继,以便稍后继续往后遍历
        p->next = L->next;
        L->next = p;	//将p结点头插
        p = r;	//p找回它原始的后继,继续遍历
    }
    return L;
}

  时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

06、

  • 有一个带头结点的单链表L,设计一个算法使其元素递增有序。

单链表的排序问题。没啥好说的啊,用一些能对单链表使用的排序算法不就行了么。比如冒泡、插入、选择排序等。——但是对于单链表能使用的那些排序算法就很有限了,基本上只能是 O ( n 2 ) O(n^2) O(n2)的。

那么我们也可以换一种思路:

首先,把单链表中的值复制到一个数组里,然后对数组进行排序,就能够使用那些 O ( n l o g n ) O(nlogn) O(nlogn)的算法,排序完之后,再复制到链表中。——这种策略显然是以空间换时间了。

算法思想:

《王道24数据结构》课后应用题——第二章_第1张图片

  采用直接插入排序算法的思想,把前i个结点视为已排好序的有序单链表(初始时i=1),然后依次扫描单链表中剩下的结点*p(直至p==NULL)为止,在前i个结点构成的已经有序的表中通过比较查找到合适的插入位置即可。为了便于执行插入操作,设置两个指针*pre,*p

void Sort(LinkList &L) {
    LNode *p = L->next;		//p用来从前往后遍历整个表
    LNode *pre;			//pre的涵义是,在前i个结点的已有序表中,p的前驱所在位置
    LNode *r = p->next;		//辅助指针r,暂存p的后继,以便执行插入操作后能继续向后遍历
    
    L->next->next = NULL;	//初始时有序表中只含1个结点
    p = r;		//p从第二个结点开始遍历
    while(p!=NULL) {
        r = p->next;		//r先保存p的后继
        pre = L;		//每轮循环,pre都从头结点L起,在已有序表中为p查找合适的插入位置
        while(pre->next != NULL && pre->next->data < p->data) {
            pre = pre->next;
        }
        p->next = pre->next;
        pre->next = p;		//将p插入到pre之后
        p = r;		//p回去继续遍历
    }
}

  该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)

  如果先将单链表元素复制到一个数组中,然后执行 O ( n l o g n ) O(nlogn) O(nlogn)的排序算法,之后再将数组元素复制到链表中。那么这种方法,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( n ) O(n) O(n),是一种以空间换时间的策略。

07、

  • 设在一个带表头结点的单链表中所有元素结点的数据值无序,试编写一个函数,删除表中所有介于给定的两个值(作为函数参数给出)之间的元素的元素(若存在)。

和第2题的思路一样,只不过把if的条件由“等于x”改为“介于两个值之间”。

08、

  • 给定两个单链表,编写算法找出两个链表的公共结点。

  首先正确理解这个问题:啥叫两个链表有公共结点。

  两个链表有公共结点,即两个链表从某一结点开始,它们的next都指向同一个结点。又因为每个单链表结点只有一个next域,所以从第一个公共结点开始,之后它们所有的结点都是重合的,不可能再出现分叉了。所以两个有公共结点而部分重合的单链表,拓扑形状看起来像Y,而不可能像X

基于这个理解,我们先想出一个蛮力法。

蛮力法思想:

  依次遍历第一个单链表中的结点,对于每个结点,到第二个链表中依次比较,看是否有相同的,若找到两个相同的结点,则找到了它们的公共结点。——这种算法的时间复杂度为 O ( l e n 1 × l e n 2 ) O(len1×len2) O(len1×len2)

如何不用蛮力,更巧妙的解决这个问题?

我们先不考虑如何找到两个单链表的公共结点是谁,我们先考虑:如何判断两个单链表是否有公共结点的存在?

对于这一问题,我们应当注意到这样一个事实:若两个单链表只要有一个公共结点的存在,那么它们在公共结点之后的所有结点都是重合的,那么它们的最后一个结点一定是重合的。

反之,我们直接看两个单链表最后一个结点是否是一样的,如果一样,则说明它们有公共结点;否则说明它们没有公共结点。

算法思想:

  因为两个链表长度不一定相同,假设一个链表比另一个链表长k个结点,那么我们先在长的链表上遍历k个结点,之后再对两个单链表同步遍历,并最终同步到达最后一个结点。显然,如果两个链表有公共结点,那么对这两个链表同步遍历的过程中,肯定也是同步到达其第一个公共结点的。因此,在同步遍历的过程中,第一个相同的结点就是第一个公共的结点。

  由此,我们先分别遍历两个链表得到它们的长度len1、len2,并求出两个长度之差k。在长的链表上遍历k个结点,之后开始同步遍历,直到找到相同的结点,或者一直到遍历结束。——该方法时间复杂度为 O ( l e n 1 + l e n 2 ) O(len1+len2) O(len1+len2)

代码:

LinkList SearchListCommon(LinkList L1, LinkList L2) {
    int dist;		//表长之差
    int len1 = Length(L1);
    int len2 = Length(L2);	//先计算两个链表的表长
    LinkList longList;
    LinkList shortList;		//分别指向较长的表和较短的表的表头
    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;		//表长的链表先遍历到第dist个结点
    }
    while(longList!=NULL) {		//同步开始遍历两表
        if(longList == shortList) {
            return longList;		//若找到相同结点,则为第一个公共结点
        }
        else {
            longList = longList->next;
            shortList = shortList->next;
        }
    }
    
    return NULL;		//遍历结束也没遇到相同结点,则不存在公共结点
}

09、

  • 给定一个带表头结点的单链表,设head为头指针,结点结构为(data, next),data为整型元素,next为指针,试写出算法:按递增次序输出单链表中各结点的数据元素并释放结点所占的存储空间(要求:不允许使用数组作为辅助空间)。

思路1:

  和第6题思想一样,先对单链表用直接插入排序法,使其递增有序,之后依次输出并释放即可。

思路2:

  不需要老老实实的进行递增排序。我可以进行 n n n轮从头至尾的遍历,每一轮遍历,找到一个最小值结点,并将其输出并释放即可。——时间复杂度也是 O ( n 2 ) O(n^2) O(n2)

void Min_Delete(LinkList &head) {
    while(head->next != NULL) {		//循环到仅剩头结点
        LNode *pre = head;		//pre为最小值结点的前驱
        LNode *p = pre->next;	//p为工作指针
        LNode *min;		//指向被删除结点,即最小值结点
        while(p->next != NULL) {		//从头至尾对单链表进行遍历
            if(p->next->data < pre->next->data) 
                pre = p;		//pre为当前最小值结点的前驱
            p = p->next;	//p继续往后找
        }
        print(pre->next->data);	//输出元素最小值结点的数据
        min = pre->next;		//元素值最小结点的指针
        pre->next = min->next;
        free(min);		//释放
    }
    free(head);		//最终释放头结点
}

注:和第6题同理,如果能够复制到数组中,然后排序,就能实现时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的方法。

10、

  • 将一个带头结点的单链表A分解为两个带头结点的单链表A和B,使得A表中含有原表中序号为奇数的元素,而B表中含有原表中序号为偶数的元素,且保持其相对顺序不变。

通过读题可知一个简单的办法。首先,另设一个单链表B;之后对单链表A进行从头至尾的遍历,设一个计数器i,当i为奇数时,不进行操作;当i为偶数时,将当前遍历到的结点尾插至表B。

算法思想:

  设置一个访问序号变量(初值为0),每访问一个结点,序号自动加1;然后根据序号的奇偶性将结点插入到A表或者B表中。重复该操作直至表尾。

它这个算法把表A也视为一个新建立的空表、往A里面也进行尾插,而不是将表B需要的结点拿走,剩余的结点不进行操作。这样可能逻辑更清晰一点,操作也更统一。

LinkList DisCreat(LinkList &A) {
    int i=0;		//i记录表A中结点的序号
    LinkList B = (LinkList)malloc(sizeof(LNode));	//创建B表表头
    B->next = NULL;		//B表初始化
    LNode *ra = A;
    LNode *rb = B;		//两个指针分别指向A表和B表的尾结点
    p = A->next;	//p为工作指针,指向待遍历的表结点
    A->next = NULL;	//A表视为空表
    while(p!=NULL) {
        i++;		//序号加1
        if(i%2 == 0) {		//序号为偶数的结点
            rb->next = p;	//尾插至表B
            rb = p;			//表B的尾结点更新
        }
        else {
            ra->next = p;	//尾插至表A
            ra = p;		//表A的尾结点更新
        }
        p = p->next;	//p指针继续回到原处,往后遍历
    }
    ra->next = NULL;
    rb->next = NULL;
    return B;		//表A已经由参数&A进行修改了,函数再将表B返回即可同时得到表A、表B结果
}

11、

  • C = { a 1 , b 1 , a 2 , b 2 , . . . , a n , b n } C=\{a_1,b_1,a_2,b_2,...,a_n,b_n\} C={a1,b1,a2,b2,...,an,bn}为线性表,采用带头结点的单链表存放,设计一个就地算法,将其拆分为两个线性表,使得 A = { a 1 , a 2 , . . . , a n } , B = { b n , . . . , b 2 , b 1 } A=\{a_1,a_2,...,a_n\},B=\{b_n,...,b_2,b_1\} A={a1,a2,...,an},B={bn,...,b2,b1}

你可能感兴趣的:(数据结构,算法,数据结构,c语言)