《数据结构习题集》所得其一

2023-11-22 20:20

本文主要是介绍《数据结构习题集》所得其一,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


·习题

#

2.12 ③
设A=(a1,...,an)和B=(b1,...,bn)均为顺序表,A'和B'分别为A和B中除去最大共同前缀后的子表(例如,A=(x,y,y,z,x,z),B=(x,y,y,z,y,x,x,z),
则两者中最大的共同前缀为(x,y,y,z),在两表中除去最大共同前缀后的子表分别为A'=(x,z)和B'=(y,x,x,z))。
若A'=B'=空表,则A=B;若A'=空表,而B'≠空表,或者两者均不为空表,且A'的首元小于B'的首元,则A<B;否则A>B。
试写一个比较A,B大小的算法(请注意:在算法中,不要破坏原表A和B,并且,也不一定先求得A'和B'才进行比较)。

这道题目的题干中,有一个很明确的算法:

 算法描述

依照这个算法可以写两个函数来完成这两个主要的步骤:

getSubL()——求出子表

cmpSubL()——比较子表


点击查看线性表的声明

 1 void getSubL(SqList *sqlA, SqList *sqlB, SqList *sublA, SqList *sublB){
 2     int lenA,lenB,minLen;
 3     lenA = sql_getLen(sqlA);
 4     lenB = sql_getLen(sqlB);
 5     minLen = (lenA>lenB)?lenB:lenA; //得到较小的线性表的长度:minLen
 6 
 7     int i = 0;
 8     while( i < minLen && sql_get(sqlA,i)==sql_get(sqlB,i) ){ //寻找第一个不相同元素的位置:i
 9         i++;
10     }
11 
12     int j = 0, temp1 = i;
13     while( temp1 < lenA ){ //从第一个不相同的元素开始构建子表:sublA
14         sql_insert(sublA,j,sql_get(sqlA,temp1));
15 
16         temp1++;
17         j++;
18     }
19 
20     int k = 0, temp2 = i;
21     while( temp2 < lenB ){ //从第一个不相同的元素开始构建子表:sublB
22         sql_insert(sublB,k,sql_get(sqlB,temp2));
23 
24         temp2++;
25         k++;
26     }
27 }


 1 int cmpSubL(SqList *sublA, SqList *sublB){
 2     int result;
 3     if( sql_isEmpty(sublA) && sql_isEmpty(sublB) ){ //两子表均为空:A=B
 4         result = 0;
 5     }else if( (sql_isEmpty(sublA)&&!sql_isEmpty(sublB))
 6     || (!sql_isEmpty(sublA)&&!sql_isEmpty(sublB)&&sql_get(sublA,0)<sql_get(sublB,0)) ){
 7     //子表A为空表且子表B不为空表  或者  均不为空表且子表A的首元素小于子表B的首元素:A<B
 8 
 9         result = -1;
10     }else{ //否则:A>B
11         result = 1;
12     }
13 
14     return result;
15 }

嗯,这样确实可以思路清晰地解决问题。

可是总觉得哪里怪怪的...

这时我注意到题目最后的小括号里的内容:并且,也不一定先求得A'和B'才进行比较。

这句话的意思就是,求子表A和子表B是没有必要的?

求子表是为了比较两线性表不同部分的关系,这样的话...为什么不能在找到第一个不同元素位置的时候就作比较呢?

完全可以啊!

在getSubL()里,

int i = 0;
while( i < minLen && sql_get(sqlA,i)==sql_get(sqlB,i) ){ //寻找第一个不相同元素的位置:ii++;
}

当这个循环结束的时候,变量 i 就是第一个不同元素的位置。

反观上边的比较算法,我发现比较其实主要是基于子表长度或子表中第一个元素进行的。

那我们完全可以只用变量 i 来完成比较!

int result;
if( i == sql_getLen(sqlA)&& i == sql_getLen(sqlB) ){ //i和表长相等就意味着子表长度为0,子表为空
result = 0;
}else if( (i==sql_getLen(sqlA)&&i!=sql_getLen(sqlB)) //同样是用i和表长作比较来判空|| (i!=sql_getLen(sqlA)&&i!=sql_getLen(sqlB)&&sql_get(sqlA,i)<sql_get(sqlB,i)) ){ //子表首元素就是i位置的元素
result = -1;
}else{result = 1;
}

这样我们就省去了很多步骤(构建子表的循环),而且省去了很多额外空间(构建子表所需的空间),使得算法的时间复杂的和空间复杂度都大大减小。

而且,我们几乎丝毫没有改变逻辑的清晰性以及代码的可读性。所以,这是一个十分飘逸的改进过程!~

最终,将两线性表的比较整合成一个函数sqlist_Cmp():

 1 int sqlist_Cmp(SqList *sql1, SqList *sql2){
 2     int i = 0;
 3     while( sql_get(sql1,i) == sql_get(sql2,i)
 4         && i < sql_getLen(sql1)
 5         && i < sql_getLen(sql2) ){
 6 
 7         i++;
 8     }
 9 
10     int result;
11     if( i == sql_getLen(sql1)
12         && i == sql_getLen(sql2) ){
13 
14         result = 0;
15     }else if( (i==sql_getLen(sql1)&&i!=sql_getLen(sql2))
16             || (i!=sql_getLen(sql1)&&i!=sql_getLen(sql2)&&sql_get(sql1,i)<sql_get(sql2,i)) ){
17 
18         result = -1;
19     }else{
20         result = 1;
21     }
22 
23     return result;
24 }

哇,这时候的代码和最初的比起来可以说是漂亮的一匹了!

通过这个题我发现:

1. 书上的提示还是很重要的呀!

2. 很多时候,有些东西是逻辑上的,为了方便思考而准备的概念;到了真正实现的时候可以根据实际,进行优化。

//2017.11.14 10:04


·思路

#

关于链表带不带头结点的思考


点击查看链表的声明

从网上看到很多文章都说,链表之所以要带头结点就是因为如果不带头结点,那第一个节点就和之后的别的节点操作不一样,这样容易引发bug什么的。开始看到时不是很理解,现在懂了和大家分享一下。


QQ截图20171118132209


假设我们现在在函数中,参考上图,先来看这样两个操作:插入到当前节点之前删除当前节点

这两个操作有一个共同点,那就是我们必须要改变前一个节点的指针域

那如果这两个操作发生在第一个节点身上的话,那它所谓的“前一个节点的指针域”就是头指针

这时,我们就已经触及到我们所说的那个“不一样的操作”了。因为在函数中,指向当前元素的指针虽都是临时变量,但是通过指针操作所得到的指针域就是确确实实的左值指针域变量了(即我们可以在函数里确确实实地改变函数外的变量本身,而不是改变它的副本了)。然而,因为头指针并不是真正的指针域,它不是链表节点的一部分,只是一个单纯的指针,所以我们无法通过一般的指针操作得到左值形式的它(得不到左值就不能对它的实体进行赋值,也就无法实际改变它的值)。故,我们用一般的传入“指向节点指针的参数”的方式无法对其完成赋值。

假设这是我们链表插入函数的声明

void insert(Node *listNode, int value)

一般情况下,这个listNode就是链表头指针的副本(因为我们一般通过insert(llist1,10)调用它),它指向的就是第一个节点。

listNode->next操作后,如上文所说,它就是第一个节点的指针域的左值了。所以,这也就是说,我们对除第一个节点之外的节点改变前一个节点指针域的操作都可以完成了,因为我们得到了所需指针域的左值(好长的一句话…)。

QQ截图20171118152548

可是,我们却不能对第一个节点做“改变前一个节点指针域”操作(废话,它是第一个节点,哪有前一个节点啊…)这也就是说,我们无法用刚才的逻辑完成第一个结点的删除和在它之前插入元素(因为这两个操作都用到了前一个节点指针域的左值)。


这样我们就无法为链表写出一个通用的插入和删除函数了。然后我们有两种解决思路:

1. 改变逻辑,即改变做插入操作和删除操作的逻辑,不用“前一个节点指针域”了,换一种新方法。

2. 改变链表结构,即本题目讨论的重点:给没有头结点的链表加上头结点。


我们已经知道了结果,我们选择的是第2种解决思路。重点在于选择的原因,而这正是头结点存在的意义

一般情况下,我们更倾向于觉得:为了给一个容器写操作函数而改变容器本身的结构是反常的。

可事实是,我们果真这么做了。为什么?因为前边那个结论成立是有个前提的:不改变结构而改变逻辑的成本低于或等于改变结构的成本。即是说,写一个不用给链表加头结点就能完成“第一个节点的删除和在它之前插入元素”操作的算法,是十分困难、麻烦并且不方便使用、难于维护的

你可能很惊讶,为啥一个算法会有这么可怕的描述,不就是不加头结点吗,有啥难的。。。

你可以先不看下边我给出的代码自己先试着写一下。我反正觉得我写这样的代码,是愣头青的行为…

.

.

.

.

.

.

.

.

.

.

好,现在我们就假装我们选择了第1种解决思路,我们头铁,我们开始写算法:

嗯…先思考,不论是删除第一个节点还是在第一个节点前插入元素,我们始终逃不过一件事,那就是要打破头指针和第一个节点之间的连接。嗯,这不难,只要改了头指针的值就行了呗。emmmm…头指针的值,好吧它是个指针,不管他是个啥吧,我们要在函数里改变它的值,只能通过取它的地址把它的地址传进来,然后通过指针运算符改那块内存的值。所以,我们就需要把头指针的地址传进来,嗯,那传进来的就是一个指向链表节点类型指针的指针了。好吧,绕是绕点,我们不管,我们头铁!写!


  1 void insert(Node **headpp, elem_type value){ //headpp是指向头指针的指针
  2 	Node **prevnextp = headpp; //prevnextp是指向前一个节点指针域的指针
  3 	Node *current = *prevnextp; //current是指向当前元素的指针
  4 
  5 	while( current != NULL
  6 		&& current->value < value ){ //按值递增插入链表,循环找合适位置
  7 
  8 		prevnextp = &(current->next);
  9 		current = current->next;
 10 	}
 11 
 12 	Node *newNode = (Node*)malloc(sizeof(Node)); //创建要插入的新节点
 13 	if( newNode != NULL ){ //检查是否创建成功
 14 		newNode->value = value;
 15 		newNode->next = current; //连接新节点和当前节点
 16 	}else{
 17 		//报错终止,具体细节略
 18 	}
 19 
 20 	*prevnextp = newNode; //连接前一个节点和新节点
 21 }


嗯…好吧这就是代码。说实话…其实也还好。好吧,它还是困难、麻烦、不方便使用、难于维护的。

为什么呢?说实话,我现在并没有觉得它有多么麻烦。可是,我怎么能保证我在不那么清醒的时候能理解它呢?这并不只是一个玩笑,因为这个函数的第一个参数是个二级指针,而链表除了init()函数之外都是传的一级指针,所以很容易在使用的时候产生误解,这是它的“不方便使用”。而它的“困难”是在于它是基于二级指针进行值操作的,基于“指针层数越多越难理解”的共识,它比起一级指针操作更难于马上理解。正是由于它的“困难”,在以后维护的过程中则会“难于维护”。而它的“麻烦”在于,本来我们是可以基于一级指针、基于已经设计好的指针域进行链表的拆解和连接的(除第一个节点),而我们就为了第一个节点的方便造成了除第一个节点外所有节点的麻烦,这并不理智。

经验告诉我们:把复杂的事情变成简单的事情而解决之比直接解决复杂的事情更优。


所以,我们选择给链表加上一个头结点。对于其他的操作函数,只需要稍加调整就可以适应它,并且依旧不会改变原来的逻辑。并且我们只是加了1个头结点,所以并不会带来任何过分的浪费,却使我们的代码翻倍地易于理解,易于维护,何乐而不为?


有了头结点,我们的第一个节点便不再孤独,因为它也和大家一样了,它也有了前一个节点——头结点。

这样我们一开始的那个逻辑又行得通了,因为我们始终可以用listNode->next来取得我们需要的左值了。

这样我们便不需要再绞尽脑汁去考虑复杂的新解决办法了,简单的办法大家都容易想到,所以代码的可读性也就提高了。终成圆满!


附代码:

  1 void insert(Node *headp, elem_type value){ //headp是头指针
  2 	Node *prev = headp; //prev是指向前一个节点的指针,初始指向头结点
  3 	Node *current = prev->next; //current是指向当前元素的指针
  4 
  5 	while( current != NULL
  6 		&& current->value < value ){ //按值递增插入链表,循环找合适位置
  7 
  8 		prev = current;
  9 		current = current->next;
 10 	}
 11 
 12 	Node *newNode = (Node*)malloc(sizeof(Node)); //创建要插入的新节点
 13 	if( newNode != NULL ){ //检查是否创建成功
 14 		newNode->value = value;
 15 		newNode->next = current; //连接新节点和当前节点
 16 	}else{
 17 		//报错终止,具体细节略
 18 	}
 19 
 20 	prev->next = newNode; //连接前一个节点和新节点
 21 }

//2017.11.18 20:02

转载于:https://www.cnblogs.com/joenahm/p/7825765.html

这篇关于《数据结构习题集》所得其一的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/412675

相关文章

【数据结构】线性表:顺序表

文章目录 1. 线性表2. 顺序表2.1 概念及结构2.2 接口实现2.3 顺序表的问题及思考 1. 线性表 线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式

数据结构9——排序

一、冒泡排序 冒泡排序(Bubble Sort),顾名思义,就是指越小的元素会经由交换慢慢“浮”到数列的顶端。 算法原理 从左到右,依次比较相邻的元素大小,更大的元素交换到右边;从第一组相邻元素比较到最后一组相邻元素,这一步结束最后一个元素必然是参与比较的元素中最大的元素;按照大的居右原则,重新从左到后比较,前一轮中得到的最后一个元素不参4与比较,得出新一轮的最大元素;按照上述规则,每一轮结

算法与数据结构面试宝典——回溯算法详解(C#,C++)

文章目录 1. 回溯算法的定义及应用场景2. 回溯算法的基本思想3. 递推关系式与回溯算法的建立4. 状态转移方法5. 边界条件与结束条件6. 算法的具体实现过程7. 回溯算法在C#,C++中的实际应用案例C#示例C++示例 8. 总结回溯算法的主要特点与应用价值 回溯算法是一种通过尝试各种可能的组合来找到所有解的算法。这种算法通常用于解决组合问题,如排列、组合、棋盘游

嵌入式学习——数据结构(哈希、排序)——day50

1. 查找二叉树、搜索二叉树、平衡二叉树 2. 哈希表——人的身份证——哈希函数 3. 哈希冲突、哈希矛盾 4. 哈希代码 4.1 创建哈希表 4.2  5. 算法设计 5.1 正确性 5.2 可读性(高内聚、低耦合) 5.3 健壮性 5.4 高效率(时间复杂度)时间复杂度越低,效率越高, 5.5 低储存(空间复杂度)空间复杂度越低,存储空间越少 6.排序算法 6.1 冒

【数据结构与算法 经典例题】使用队列实现栈(图文详解)

💓 博客主页:倔强的石头的CSDN主页               📝Gitee主页:倔强的石头的gitee主页    ⏩ 文章专栏:《数据结构与算法 经典例题》C语言                                   期待您的关注 ​​ 目录  一、问题描述 二、前置知识 三、解题思路 四、C语言实现代码 🍃队列实现代码:

数据结构:二叉树详解 c++信息学奥赛基础知识讲解

目录 一、二叉树的定义 二、二叉树的形态 三、二叉树的性质 四、二叉树的存储 五、二叉树的创建与遍历(递归) 六、二叉树实现 创建二叉树 展示二叉树 1、计算数的高度 2、计算数的叶子数量 3、计算数的宽度 4、层次遍历 5、前序遍历 递归写法 非递归写法 6、中序遍历 递归写法 非递归写法 7、后序遍历 递归写法 非递归写法 8、输出根节点到所有叶

Java数据结构4-链表

1. ArrayList的缺陷 由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。 2. 链表 2.1 链表的概念及结构 链表是一种物理存储结构上非连续存储结构,数据元素

大学生自救数据结构与算法(py实现)——01递归

目录 目录 递归 基本概念 工作原理 基本要素 优点 缺点 实现技巧 实例解析:计算阶乘 斐波那契数列 高效的斐波那契数列 python中的最大递归深度 二分查找 基本原理 性能分析 优化与变体 线性递归  元素序列的递归求和 二路递归 二路递归的基本概念 典型应用 工作原理 多重递归  示例:计算卡特兰数(Catalan Number) 尾递

数据结构和算法(1) ---- Queue 的原理和实现

Queue 的定义和结构 队列(Queue) 是只允许在一端进行插入,在另一端进行删除的线性表 队列是一种先进先出(First In First Out)的线性表,简称 FIFO(First IN First OUT), 允许插入的一端称为队尾, 允许删除的一端称为队列头 队列的基本结构如下图所示: Queue 的抽象数据类型 队列也有线性表的各种操作,不同的是插入元素只能在队列尾,删除

从零开始学数据结构系列之第三章《平衡二叉树基础概念》

文章目录 前言什么是平衡二叉树往期回顾 前言 ​   在前面的学习过程中,我们了解到二叉排序树可以在一定程度上提高查找(搜索)的效率,但仍然会出现特殊情况,让二叉排序树失效。例如,将序列{1,2,3,4,5,6}中的元素依次插入到二叉排序树中,会得到右斜树,这就相当于一个单链表了,搜索效率降低为O(n)。   于是在 1962 年,一个姓 AV 的大佬(G. M. Ade