请简述深度优先遍历、广度优先遍历的基本思想?:
深度遍历是在图中先选择一个顶点,随后的每次遍历中选择与顶点相邻并且还没有遍历过的结点进行遍历,类似于树的先序遍历
广度遍历是先在图中选择一个顶点,并加入队列中,然后向该顶点的所有未访问过的邻接点进行扩散,加入到队列当中,类似于树的广度遍历
简述二叉树,完全二叉树,二叉排序树,平衡二叉树的特性:
二叉树(Binary Tree):要求其任意节点的子节点数量不超过2(分左节点即左子树和右节点即右子树)。
完全二叉树(Complete Binary Tree):要求所有叶子点都在最后一层或者倒数第二层且最后一层的叶子结点在左边连续,倒数第二层的叶子结点右侧连续。
二叉排序树(Binary Search Tree):也叫二叉搜索树,如果左子树不空,则子树上面的所有节点的值均小于或者等于根节点的值。而且如果右子树不空,则右子树的所有节点的值均大于或者等于根节点的值。其根节点的左右子树都是二叉排序树或者是一颗空树。
平衡二叉树(Balanced Binary Tree):也叫AVL树,它是一颗空树,或者其左右子树高度差绝对值不超过1,即只有度位0或者1的二叉树,其左右的两个子树都均为平衡二叉树。
简述什么是队列的上溢出现象和假溢出现象,解决它们的办法有哪些:
先进先出(FIFO)在队列的顺序存储结构中,设头指针为front,队尾指针为rear,队的容量(存储空间的大小)为m。当有元素加入到队列时,若rear=m(初始时rear=0),则发生队列的上溢出现象,该元素不能加入到队列中。
队列的假溢出现象是指队列中虽然还有空余的空间,但元素不能进队列——造成这种现象的原因是由于队列的操作方式所致。
解决队列上溢的方法有:
建立一个足够大的存储空间,但这样做往往造成空间使用效率低
当出现假溢出时,可采用以下几种方法:
采用平移元素的方法。每当队列中加入一个元素时,队列中已有的元素向队头移动一
个位置(当然要有空余的空间可移);
每当删除一个队头元素时,则依次序移动队中的元素,始终使front指针指向队列中
的第一个位置;
采用循环队列方式。把队列看成一个首尾相邻的循环队列,虽然物理上队尾在队首之
前,但逻辑上队首仍然在前,作插入和删除运算时仍按“FIFO”的原则。
建立一个足够大的存储空间,但这样做往往造成空间使用效率低。
简述单链表设置头节点的作用是什么?(至少说出两条好处):
链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。“链表中第一个结点的存储位置叫做头指针”,如果链表有头结点,那么头指针就是指向头结点数据域的指针。
头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。
有了头结点后,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。
首元结点也就是第一个元素的结点,它是头结点后边的第一个结点,而且头结点不是链表所必需的。
在线性表的链式存储结构中,头指针是指链表指向第一个结点的指针,若链表有头结点,则头指针就是指向链表头结点的指针。
头指针具有标识作用,故常用头指针冠以链表的名字。
无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
若较频繁地对一个线性表进行插入和删除操作,该线性表宜采用何种存储结构? 为什么?:
若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,用顺序存储结构效率会高很多。
为什么会引入线索二叉树?它有什么优势?:
对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1条分支线树,即其实是存在2n-(n-1)=n+1个空指针域。这些空间不存储任何事物,造成空间的浪费,就想到了线索二叉树。
线索二叉树:指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。其中对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。 对于有大量的插入和查找的二叉树来说,线索二叉树比较适用。
当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,用顺序存储结构效率会高很多。
优点:因为二叉树在遍历的时候,如果你想找到某个节点的前驱节点,就必须把整个二叉树全部遍历一遍,而线索二叉树相当于一个双向链表,可以很简单的找到某个节点的前驱节点;通常使用中序线索二叉树,因为通常叶子节点有空节点,根据中序遍历(左->中->右的遍历特点)很容易几乎将整个二叉树连接起来.
循环比递归的效率一定高吗?:
递归与循环是两种不同的解决问题的典型思路。当然也并不是说循环效率就一定比递归高,递归和循环是两码事,递归带有栈操作,循环则不一定,不同场景做不同的尝试。
递归算法:
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(有可能出现堆栈溢出),比如参数传递需要压栈等操作,会对执行效率有一定影响。对于某些问题,不使用递归,代码会艰涩难懂。
循环算法:
优点:速度快,结构简单。
缺点:不能解决所有问题。有的适合使用递归而不是循环,使用循环并不困难的话,最好使用循环。
递归算法和循环算法总结:
一般递归调用可以处理的算法,也通过循环去解决常需要额外的低效处理。
现在的编译器在优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
递归和循环两者完全可以互换。如果用到递归的地方可以很方便使用循环替换,而不影响程序的阅读,那么替换成递归往往是好的——比如求阶乘的递归实现与循环实现。
请比较 AOE 网与 AOV 网?:
从定义上来看,很容易看出两种网的不同,AOV网的活动以顶点表示,而AOE网的活动以有向边来表示,AOV网的有向边仅仅表示活动的先后次序。纵观这两种网图,其实它们总体网络结构是一样的,仅仅是活动所表示的方式不同,因此可以猜想从AOV网转换成AOE网应该是可行的。
通常AOE网都是和关键路径联系在一起的,在AOE网中我们可以通过关键路径法来计算影响整个工期的关键路径,达到缩短工期的目的。在传统的AOV网中是没有表示活动时间的权值的,因此传统的AOV网无法估算工期,但是如果我们在AOV网中的活动结点上都标上时间属性,那么AOV网就可以完全转换为AOE网。
栈和队列的异同点?:
栈与队列的相同点:
1.都是线性结构。
2.插入操作都是限定在表尾进行。
3.都可以通过顺序结构和链式结构实现。、
4.插入与删除的时间复杂度都是O(1),在空间复杂度上两者也一样。
5.多链栈和多链队列的管理模式可以相同。
栈与队列的不同点:
1.删除数据元素的位置不同,栈的删除操作在表尾进行,队列的删除操作在表头进行。
2.应用场景不同;常见栈的应用场景包括括号问题的求解,表达式的转换和求值,函数调用和递归实现,深度优先搜索遍历等;常见的队列的应用场景包括计算机系统中各种资源的管理,消息缓冲器的管理和广度优先搜索遍历等。
3.顺序栈能够实现多栈空间共享,而顺序队列不能。
4.栈先进后出,队列先进先出
栈在后缀表达式求值的算法思想:
算法思想:遍历整个表达式如果是操作数,入栈。如果是操作符,将当前栈顶元素和栈第二个元素出栈进行运算,并将结果压栈。若是除(减)操作符,第二个元素作为被除数(被减数),栈顶元素作为除数(减数);。表达式遍历完后,当前栈的栈顶元素即为所求表达式的值。
Dijkstra 算法和 BFS 求的最短路径有什么区别吗?:
dijkstra算法是求单源点的最短路径问题,要求权值不能为负;bfs算法则是从某顶点出发按广度优先的原则依次访问各连通的顶点,图可以无权值
dijkstra是bfs的升级版,就是说如果求最短路径,当图从无权值变成有权值时,bfs不再适用了,于是我们用dijkstra方法。换句话说,对于无权值图,dijkstra方法跟bfs是一致的。你可以画个无权图,用dijkstra走一遍,发现其实这就是bfs。
简述什么是哈夫曼树?:
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。
树的带权路径长度(WPL)为树中所有叶子结点的带权路径长度之和。构建哈夫曼树是从叶子结点开始,不断地构建新的父结点,直至树根,所以结点中应包含指向父结点的指针。使用哈夫曼树时是从树根开始,根据需求遍历树中的结点,因此每个结点需要有指向其左孩子和右孩子的指针。
时间复杂度为 O(nlogn)的排序方法?:
快速排序
从数组中取出一个数,称之为基数
遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
将左右两个区域视为两个数组,重复前两个步骤,直到排序完成
按照上述规则,每轮中的基数都被调整到其最终的位置上:第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数(两个子数组中有两个基数),第三轮遍历排好 4 个基数(四个子数组中有四个基数)… 因此总遍历轮数为logn ~ n 次
归并排序
针对两个无序数组:开辟一个长度等同于两个数组长度之和的新数组,并使用两个指针来遍历原有的两个数组,不断将较小的数字添加到新数组中,并移动对应的指针即可。
针对一个无序数组:我们可以把数组不断地拆成两份,直到只剩下一个数字时,这一个数字组成的数组我们就可以认为它是有序的,两个对于两个由一个数字组成的数组我们就可以使用归并排序得到一个有两个数字的有序数组;然后再将这些拆分的数组不断的两两组合起来,就完成了归并排序。
堆排序
用数列构建出一个大顶堆,取出堆顶的数字;
调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
循环往复,完成整个排序。
希尔排序
将待排序数组按照一定的间隔进行排序,如此时排序间隔为gap:则从index=gap处的元素开始排序;对于index = i的元素,每次和当前index - gap处的元素进行比较,直到符合插入条件;然后继续比较index = i++的元素,直到数组末尾,完成当前间隔的排序
逐渐缩小间隔进行下一轮排序
最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的「宏观调控」,数组已经基本有序了,所以此时的插入排序只需进行少量交换便可完成
排序算法稳定性的定义?有那些不稳定排序?:
稳定排序:排序前后两个相等的数相对位置不变,则算法稳定
非稳定排序:排序前后两个相等的数相对位置发生了变化,则算法不稳定
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法
堆排序是否是一种稳定的排序方法?为什么?:
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法
比较直接插入排序算法和希尔排序算法的不同点?:
直接插入排序
算法描述:顾名思义,直接插入排序就是将待插入的数据插入到该数据之前的有序序列的正确位置处,使得序列依然有序的排序算法。当数据有序且不需要移动是时间复杂度为O(n),当为逆序时,每次插入都需要移动数据,效率最差,则时间复杂度为O(n2)。在插入过程中,需要用一个临时的辅助空间来保存待插入的数据,所以该算法的平均时间复杂度为O(n2),空间复杂度为O(1)。另外,由于数据是一个一个逐个插入,所以这还是一种稳定的排序算法。
希尔排序算法
算法描述:希尔排序是按其设计者希尔(Donald Shell)的名字命名,它是一种基于插入排序的快速排序算法,是对直接插入排序时间复杂度上的优化。希尔排序通过将需要排序的数据分为若干个区域来提升插入排序的性能,每次能够使数据前进给定的步长,然后每次减小步长再进行插入排序,最后变为一个普通的插入排序,但到了这时,数据已经变得局部有序,所以此时的插入排序比较快。
对于存储结构,可能会联想到前面的顺序存储和链式存储结构。但是对于数这种可能会有很多孩子的特殊数据结构,只用顺序存储结构或者链式存储结构很那实现,那么可以将这两者结合,产生主要的三种存储结构表示法:双亲表示法、孩子表示法、孩子兄弟表示法
1)凡出现左括弧,则进栈;
2)凡出现右括弧,首先检查栈是否空
若栈空,则表明该“右括弧”多余,否则和栈顶元素比较,若相匹配,则“左括弧出栈” ,否则表明不匹配。
3)表达式检验结束时,若栈空,则表明表达式中匹配正确,否则表明“左括弧”有余
贪心算法和动态规划以及分治法的区别?:
分治法与动态规划的相同点:
分治法与动态规划,二者要求原问题具有最有子结构,都是将问题分而治之分解成若干个规模较小的子问题;
不同点:
动态规划是将原问题分解为多个子问题,通过计算出子问题的结果构造一个最优解。动态规划通过迭代法自底向上求解,动态规划将分解后的子问题理解为相互间有联系,有重叠的部分;
算法的应用:装配线,矩阵乘法,最长公共子序列,构造最优的二叉树
分治法是将原问题分解为多个子问题,利用递归对各个子问题独立求解,最后利用各子问题的解进行合并形成原问题的解。分治法将分解后的子问题看成是相互独立的。
例如:在求解斐波那契数列过程中 ,求解fibonacci(5)求解fibonacci(5)时,得求解fibonacci(4)和fibonacci(3).其中,求解fibonacci(4)时,需要求解fibonacci(3).
在分治法中,求解完fibonacci(4)后还得重新求解fibonacci(3),即使求解fibonacci(4)子问题的时候已经求解过,也就是说,求解了二次子问题fibonacci(3).
贪心算法:依赖于当前已经做出的所有选择,采用自顶向下(每一步根据策略得到当前一个最优解,保证每一步都是选择当前最优的)的解决方法。
贪心算法的应用:最小生成树,最短路径,数据压缩–哈夫曼编码
有哪些哈希函数的构造方法,列举一些?:
直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。
除留余数法
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 H(key) = key % p, p < m。
数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。
折叠法(叠加法)
将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。
用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。
随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。
如何由遍历序列构造一棵二叉树?:
在先序遍历序列中,第一个结点一定是二叉树的根结点;而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。
根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子序列的根结点,右子序列的第一个结点是右子树的根结点。
如此递归地进行下去,便能唯一地确定这一唯一二叉树