最近找工作,刷了下《剑指offer》这本书,上面有挺多经典的算法,对每一章做了一个总结,章节笔记见一下链接:
http://note.youdao.com/noteshare?id=ad655fcf73eb4b63e7e0700daa01d02f
下面是对整本书的一个算法,从数据结构上进行分类,这样拿到算法题的时候知道从哪一个类别去思考相似的可能性。
一、线性表
1、数组
2、链表
二、字符串
三、栈和队列
四、树
五、查找和排序
六、动态规划
七、细节实现题
八、回溯法
对于一个有序的二维数组的搜索,如果二维数组每一行都从左到右递增排序,每一列都从上到下递增排序,如果快速的搜索到数组中的元素???
从数组的左下角或者右上角进行搜索,通过大小判断去除当前行或者列,缩小搜索的二维数组的大小,最后确定搜到元素。
————————————————————————————————————————————
问题:输入数据,调整数组的顺序,奇数在前,偶数在后
解决:这个问题看起来很简单,最直观想到的方法就是从前往后遍历,偶数则提出来,放到最后,其余数据向前移动,直到遍历到末尾,这样的时间复杂度是O(n^2).
那么有没有改进的办法呢???这一来移动的问题,改进就是用空间来换取时间,方法就是交换。
设置两个指针,一个从前往后,一个从后往前,前指针找偶数,后指针找奇数,碰到奇偶则交换,指针碰头则便是重排序完成。
这种方法很好了,但是如果出题者还想考察扩展性,这个时候应该怎么办呢?出题者认为排序的方法是可以变动的,现在是奇偶,后面可能是正负等,应该如何做呢????
这就用到了设计模式的一种,将变动的提取出来, 即将重排序的方法写成一个函数。每次比较就调用这个函数就可以了,注意使用的是函数指针。
值得注意的是函数指针的用法,用函数指针来代替函数,则每次实际执行的是函数指针,那么函数对外传参就是一个指针,我们只需要将这个指针指向不同的函数即可,不需要改变函数内部的代码,函数指针是c语言里面用来封装变化量。
————————————————————————————————————————————
问题:判断数组中出现次数超过一半的数字
这个问题拿到手,看起来简单,遍历就可以,但是显然这样的时间复杂度是不符合出题者考察的,需要改进。
仔细思考,数组中出现次数超过一半的数字,那么排序后,这个数字肯定是在数组的中间,也就是中位数,所以这个问题就转为求数组中中位数,数组中任意第K大的数字,之前有提过,可以利用快排的思想去解决。
利用计数,遍历一遍,相同的数字+1,不同的数字-1,归零的时候记录下一个数字,并置计数器为1.
————————————————————————————————————————————
问题:求数组中最小的k个数
解决:首先想到的还是快排的思想,找最小的k个数,左边的区间即可,这样的复杂度页很低o(n),唯一的缺点是会改变数组的内容,可能不太需要。
第二个方法,就是基于辅助空间桶来达到降低复杂度的目的,桶存储k个数据,这k个数据在桶里面存储只要保证查找,删除,增加的时间复杂度是lgk,那么对于n个数找到最小的k个数的复杂度就可以降为nlgk.
那么这种存储方法如何选择呢?选最大值肯定想到是最大堆,然后就是树。可以用STL去完成
————————————————————————————————————————————
问题:把数组排列成最小的数
解决:这个问题,首先相当的是全排列,给出每一种全排列,然后依次比较每个全排列即可,但是这种方式的n个数字就是n!的数,显然不是面试官想要的方法。
更好的方式,是直接通过排序规则,把数组排列成最小的数。
例如,数组为{3,32,321},那么我们最后需要排列的是{321,32,3},然后将它连接起来输出即可。很明显我们需要做的是自己定义个比较的规则,这三个数如何比较得到大小,按照位数进行比较,给出比较规则,直接利用数组的排序即可解决
————————————————————————————————————————————
知识点7:
问题:求数组中的逆序对
解决:这个问题看起来非常的麻烦,因为要求数组中逆序对,那么首先想到的就是遍历,每遍历到一个数字的时候,就对它的后面的数字进行比较,找逆序对,这样的时间复杂度肯定就是o(n2)
那么有没有什么方法可以减少复杂度呢??
有:方法就是归并法
首先将数组进行划分,划分为两个子数组,那么如果这两个子数组自身的逆序对知道的情况下,我们只需要对这个两个子数组进行逆序对的获取即可,不用管他们本身的逆序对,按照这个思想,不断的划分 ,知道最后单个数字,这样做比较的次数就是归并排序的复杂度。
————————————————————————————————————————————
问题:数组中只出现一次的数字
解决:这个问题很巧妙,为什么这么说呢?如果一个数组中只有一个数字出现一次,其余都是两次,那么根据异或的思想,相同的数字都会被置0,最后只会得到一次的数字。
如果有两个数字只出现一次呢???
那么我们需要对这个数字进行一个分组,分组的依据就是根据异或的结果中为1的值,进行寻找分组。
————————————————————————————————————————————
问题:数字在排序数组中出现的次数
解决:只要是排序数组,首先想到的就是二分法,因为排序二分法的效率就会非常的高,如何才能统计出现的次数negative??
方法就是找到相同数字的第一个个末尾一个,也是用二分的方法当前匹配的中间数字的前面还有没有相同的,有的话在到前面去找,用这种方法,确定数组中前后两个指针,相减就是出现的次数
————————————————————————————————————————————
问题:给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
解决:
遇到这个题目,首先的思想是用一个双端队列取模拟这个窗口,并且始终保证队列的队首是这个窗口的最大值,然后每次滑动窗口时候将最大值取出,放入容器中。
每次新的元素进入队列的时候判断这个元素和队列对尾元素比较,哪个小,如果队列中小的元素,将其从队列中取出,如果比队列中元素小,则直接插入到队列,保证队列中元素队头的是最大的,然后开始滑动,当窗口滑动的时候,实际上窗口中的最大值一直在队列头部。
————————————————————————————————————————————
问题:在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解决:重复的数字,这个问题一拿到手想到的就是hash表来存放出现的次数。或者更简单的是bitmap,当bitmap命中的时候,那么它就是第一个重复的数字,这样做时间复杂度o(n),缺点是需要额外的辅助空间
更加巧妙的方法:很少见,这个方法是利用数组进行判断,因为题目说明所有数子小于n,那么我们当一个数字被访问后,不需要直接用辅助数组去标记,直接使用+n的方法,当超过了n的数字就是已经重复了的。
但是这种的缺点 是改变数组本身的值,但是不需要额外的辅助空间,时间效率是o(n)
————————————————————————————————————————————
问题:单向链表的反向输出,即从尾到头的输出。
解决:
由于单向链表,给予都是头指针,头指针从头到尾依次输出,所以想到的就是遍历就是从头到尾,输出从尾到头,这就是先进后出的规则,典型的栈啊!!!所以用栈去做一个遍历的保存节点,再依次从出栈输出即可:
既然想到了用栈来实现这方法,那么递归肯定也能实现,应该递归结构从本质上来说就是栈结构,所以可以用递归来实现,如果输出某个节点,先递归输出它后面的节点即可:
问题:
这样做代码简单,的确很好,但是存在的问题就是这种方法并不是尾递归,如果链表非常长的时候,会导致函数调用的层级很深,从而导致函数的调用栈溢出。
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
尾递归的原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
http://blog.csdn.net/zcyzsy/article/details/77151709
————————————————————————————————————————————
问题:给链表的头结点,待删除节点,进行删除节点的操作:
解决:
分析这个问题,看起来很简单,就是链表的删除操作,从头结点遍历到待删除节点的前一个节点,将前节点下一指针指向删除节点的后节点,然后删除待删除节点即可。这种方法显而易见,需要考虑出题人是不是为了考虑这个,显然不是,因为遍历这种方法的复杂度肯定是O(n),而出题者的意图肯定是想办法去减少这种删除操作的复杂度。
方法:仔细分析,为什么要遍历,因为单向链表我们找不到删除节点的前驱节点,所以从头结点开始遍历才能找到,但是我们可以换一个思维去想,删除节点不一定要删除当前节点,可以从后一个节点覆盖前面的节点,这样当前删除节点被覆盖也是被删除了,然后将后一个覆盖的节点就删除了。
总体的思想:先找到待删除节点的后一节点,从后一节点覆盖待删除节点;将待删除节点指向后一节点的后一节点;删除后一节点,即可。
这种方法没有进行遍历,所以时间复杂度是O(1)。
但是这种方法有两种情况需要考虑。
第一种如果是尾节点删除,此方法不能执行,还是按照原来的遍历删除。
第二种是头节点删除,则可以采用此方法,但是需要把头结点指针重新指向到当前的头结点上。
代码如下:
————————————————————————————————————————————
问题:这一类问题很多,例如本题的查找链表倒数的k个节点,求链表的中间节点,判断链表是否形成环状的结构???
解决:这一类的问题都可以采用两个指针来完成,即增加一个指针,让其中一个指针遍历的速度快些,例如一次遍历走两步,或者让它在链表上先走若干不,这样可以控制两个指针的间距,当一个指针到尾部的时候,另一个指针变可以得到遍历的点,加快遍历。
代码:
注意:
需要注意的是,链表的代码都需要鲁棒性的保证,即进行防御性的编程,对于编程可能抛出的异常的地方都需要进行留意,观察,并提前做好防御性的准备。
————————————————————————————————————————————
问题:如何将一个单向链表反转???
解决:这个问题看起来不难,但是其中比较难解决的是链表是单向的,如果将节点的下一个指针反转,那么后面的节点就无法访问了,出现了链表的断裂,所以必须得要一个指针指向后面的节点,同时前节点,也需要指针指向,那么一共就需要有三个指针,也就是说,链表的反转需要有三个指针。
————————————————————————————————————————————
问题:如何合并两个已经排序好的链表,合并完也是排序好
解决:这个问题之前就已经见到了,从头到尾的比较两个链表,每次取出最小的节点,当某一个链表为空的时候,这个时候可以将另外一个链表完成放入到合并链表的后面,即完成了合并。
显然这种方法:可以使用递归,也可以使用循环。
递归代码:
循环迭代代码:
————————————————————————————————————————————
问题:复杂链表的复制,复杂链表指的是多一个任意指针的链表
解决:首先用实例分析具体的过程,然后利用分治思想,将这个过程划分为三个部分,首先是拷贝原结点,并将拷贝结点放在原结点后面;然后进行任意结点的拷贝,任意结点指向原指针的后一个;最后将链表拆分,奇数个即为复制的链表。关键是要画图分析,很重要。
————————————————————————————————————————————
问题:两个链表的第一个公共节点
解决:这个问题通过画图来看,吐过两个链表有公共节点,那么后面他们都是公用的,最后形成的就是一个Y字。
这种我们知道,从链表的后面开始遍历,效果更好,有没有更好的方法,答案是有的,就是加快指针遍历,一个指针遍历块,一个指针遍历慢,当他们遍历到相等的时候,就是公共节点。
————————————————————————————————————————————
问题:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
解决:这个题很巧妙,因为没问你删除的节点是什么,只是要做到一个删除节点的链表,那么我们的思路就比较简单,可以就遍历一个节点,然后将这个节点的下一个节点指向不重复的节点上,重复的节点跳过就可以,到达尾节点即可
————————————————————————————————————————————
问题:一个链表中包含环,请找出该链表的环的入口结点。
解决:这一题之前有遇到过,解释也比较清楚,分两步,第一个确定相遇点,第二个确定环的入口节点。
————————————————————————————————————————————
问题:对于字符串进行原址替换,即在原来字符串的基础上替换特定的字符,例如用十六进制替换空格。
解决:
直观的想法,从前到后,碰到替换的字符串,则后面的字符依次向后替换,但是这种做法,极端的情况下,如果有n个替换字符,则会移动后面的字符n*n次,显然不够好
从前往后的做法,会导致重复的移动,改进的办法则是从后往前的替换。
首先:计算好最后替换完的尾指针在哪
然后,将非替代的字符依次移动在最后的尾指针
最后,碰到替换字符,则直接替换即可。
这种方法,需要额外的依次遍历确定尾节点,但是最后移动替换的开销只有O(n)
总结点:合并数组,原址替换,这些从效率上考虑,从后往前替换效果较好。
————————————————————————————————————————————
问题:面试题是关于n位的整数,且没有限定n的取值范围,或者是输入任意大小的整数,例如顺序打印任意n位整数???
这一类的问题都称为大数问题,因为题目没有指定具体的数值上限,也就是说这个整数可能很大,大道没有c++中没有响应的数据类型能够存放的下,那么应该如何办???
解决大数问题最常见的方法就是用字符串,用字符串来存放大数问题。
通过用字符串来存放大数,然后在字符串上模拟数字的加法,解决题目的问题,例如顺序打印任意n位整数。首先创建一个n位字符串,每个字符保存位数值,然后用字符串模拟数字的+1,打印结果:
需要注意的是,就是用字符来模拟数字的加减的时候,需要注意高位的进位问题,当最高位发生了进位,则表示已经遍历输出到了最大数,此时程序循环应该停止。
上述的代码量还是挺大,也需要很多操作,有没有简单代码量的解决方法????
将问题转化为数字排列的问题,使用迭代方法去优化上述的问题。
因为顺序输出n为的数字,实际上就是每一位都0-9进行全排列组合的问题,10^n个数。
对于全排列的问题,我们完全可以用递归来实现,递归的最底层是顺序输出一位上的0-10.呢么我们可以一位一位的递归处理,先处理低位,低位处理完了,在处理高位,每位+1,然后调用低位遍历输出。
这里面试者更多的是考虑,会不会用递归的思想去解决实际问题的能力。
————————————————————————————————————————————
问题:全排列问题,即输入一串字符串,求这个字符串的全排列
解决:
这个问题需要用到分治的思想,思想很简单,首先确定首位字符,然后把首位字符和后面的不同字符进行交换,第二步,将后面的看做一个整体,求后面的排列,使用的也是交换的原理,直到交换到最后一个字符即可。
注意:前面交换完成了之后,后面要交换回来:
https://www.cnblogs.com/zyoung/p/6764371.html
这个问题可以演变成其他的很多问题,
例如:如果 一个面试题是按照一定的要求拜访若干个数字,我们可以先不管摆放的规则,先求出这个写数字的所有排列,然后再一一判断每个排列是不是满足题目给的要求。
复杂问题的解决思想:将复杂问题分解成为若干个小问题,然后递归的解决这些问题,例如可以使用分治法和动态规划方法。
————————————————————————————————————————————
问题:从1到n整数中出现1的次数
解决:
方法1:暴力破解,遍历所有1-n,判断是不是包含1,包含则+1
方法2:从高位到低位,划分区间,按照1依次进行判断
————————————————————————————————————————————
问题:第一个出现一次的字符
解决:显然这是一个计数的问题,给每个字符计数,那么输出第一个计数为1的数字就是第一个出现一次的字符,那么用什么去存储每个字符的计数值呢?显然就是hash表,或者是bitmap.
————————————————————————————————————————————
问题:翻转单词的顺序,或者是左旋转字符串
解决:这一类的问题都是通过两次交换完成的,首先遍历整个字符串,头尾交换,然后遍历单词内部,头尾交换,就可以得到结果:
————————————————————————————————————————————
问题:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
解决:对于这个题目,区别之前的找到数组中不重复的数字,那个题目用异或很简单,这个题目难的是,找到的是第一个出现的不重复的字符,如何做?
建立一个队列和一个数组,数组记录出现的次数,队列存放字符流,每次字符流出队列时候就去判断字符对应次数是多少,超过两个则从队列中删除,1个则保留在队里中,那么队首的元素一定是第一个不重复的元素。
————————————————————————————————————————————
问题:如何通过两个栈来完成一个队列???
解决:思想很简单,将入队列的数据依次放入栈1,然后将栈1的数据依次放到栈2,那么在栈2中最先入栈的数据便到了栈顶,依次弹出即可。
注意:
1.代码上:这里写的模板类,因为如果单纯写数据结构是可以不考虑数据的类型的,可以写成模板类。
2.写代码是必须考虑到栈为空的情况,同时需要注意的是栈有可能是满的,也就是说我们的队列深度就是单个栈的深度,两者必须得是一样的。
问题:如何通过两个队列完成一个栈?
解决:这个问题和问题9是相反的,考虑到栈是后进先出,而队列是先进先出,那么假设开始数据abc都进入到了队列1,这个时候需要弹出数据c,因为是最后进,自然的想法就是将a,b都导到队列2中,然后队列1中c直接可以弹出。然后每次添加元素的时候,可以直接加入到非空的队列中,非空的队列将元素导入到空的队列上,此时剩余的元素即是出列。
注意代码:注意空栈此时弹数据的异常处理,如果两个队列同时有数据也应该报异常 处理。
有没有可以改进的地方:
当添加新的数据的时候,可以采用这种方法:将新的数据加入到空的队列q2上,然后移动非空队列q1到空的队列q2上,这样最后加入的数据可以最小从队列q2中弹出,减少这个数据本身的移动时间。
缺点:如果是遇到了连续的加入数据,就会导致频繁的从q1,q2中迁移,造成了开销。
这种方法的具体复杂度取决于数据插入删除操作的间隔频率
http://blog.csdn.net/jiange_zh/article/details/50583370
————————————————————————————————————————————
问题:如何设计一个栈,除了包含push/pop之外,还包含min函数??
解决:这个问题首先很直观的感受是对原来栈里面的元素进行排序,然后每次让最小的元素在栈顶,这样当调用栈顶元素的时候,就是最小的元素。但是这样做很明显有个问题,问题就是不符合原来栈先入后出的规律,这样显然是不可行的。
真正的解决办法是在创建一个栈,用来保存每次进栈数据的最小值,如果进栈数据大,就保存上次的最小值,即保证辅助栈中从栈顶到栈底都是最小值,从小到大。
那么此时设计的min函数,就是从辅助栈从弹出栈顶元素即可,也满足时间复杂度O(n)
————————————————————————————————————————————
问题:给出两个序列,一个是栈的压入序列,一个栈的弹出序列,判断这种 压入弹出顺序是否可行
解决:
这个问题,看起来不好解决,我们可以通过花个图来解决这类问题,拿一个具体的例子去看,就可以找到问题的解决办法。
首先在弹出序列之前所有的数据都压入栈,判断是否可以弹出,如果不符合,则继续压数据,知道有弹出的数据,如果全部都压入栈了,还没有符合的弹出序列,则不符合。
————————————————————————————————————————————
问题:如何利用前序和中序序列,重新构造出一个新的二叉树???
解决:
解决办法其实也是用到了递归的思想,首先我们知道前序是根左右,中序的左根右,那么通过前序可以找到根,用根去遍历中序,则在中序中根元素左边的序列即为根的左子树,在根元素右边的序列即为根的右子树。这样即找出了左右子树。利用递归的思想,在找出的左右子树各自的序列中在进行左右子树的划分,一直递归,知道找到左右叶子节点,那么即完成了树的构建。
1.首先找根
2.在中序上根据根找对应的左右子树
3.对左右子树的序列递归进行下一层左右子树的调用
完成重建一棵树的目的。
写代码上值得注意的是:抛出异常的处理:
————————————————————————————————————————————
问题:判断一棵二叉树是不是另外一棵二叉树的子结构
解决:问题很简单,主要是考察树的操作,其中主要涉及的是树的遍历,判断两个树的子结构是不是相同的。
所以首先想到的思路是:判断根节点是不是相同的,然后如果相同进而转去判断这个根节点的所有子节点是不是相等,可以采用递归的思路去编写代码。
代码如下:
————————————————————————————————————————————
问题:二叉树的镜像操作
解决:这种非常规的操作,首先需要做的就是画图,画图来看找到解决问题的思路,通过画图很明显可以判断镜像就是非叶子节点的所有互换。
即可以用递归的思想去从上到下完成,从顶点向下递归对非叶子节点的左右节点交换。
能用递归完成的,就能用栈去迭代完成
————————————————————————————————————————————
问题:从上往下打印出二叉树的每个节点,同一层的从左往右的顺序打印
解决:
这个问题其实就是一个广度优先遍历的问题,不管是图还是二叉树,广度优先遍历利用的就是队列,先进入根节点先打印根节点,而深度优先遍历则相反,利用的是栈的性质,先进入的根节点后打印。
所以这个问题就提供一个辅助的队列即可,先进入根节点,然后在循环中每次从队头取一个数据打印,通过判断取的这个数据有没有节点,有节点则将节点数据从左往后依次加入队列,最后队列全部出列,则遍历一遍完成。
————————————————————————————————————————————
问题:根据二叉树的后序遍历方式来判断是否能构建出二叉树
解决:
这种问题和前面的构建二叉树其实是一样的,考察的就是根据二叉树的遍历方式,来构建一个二叉树,思想是首先通过遍历方式确定根节点,然后将其分为左边一组和右边一组,然后左边一组再次根据遍历方式确定 根节点,这样递归,如果都能分组成功,最后就能形成二叉树,反之则不能。
————————————————————————————————————————————
问题:二叉树中和为某一个值的路径?
解决:这个问题咋一看来,比较麻烦,二叉树的路径好像很少涉及到,这一类问题还是用具体的例子来分析,要知道全部的路径,我们当然得需要一个容器来保存这个路径,根据题目可以知道所有的路径都是从根节点出发的,所以我们得采用前序遍历去访问二叉树,在访问的时候,我们将访问的节点都依次放入的容器中,然后给出容器节点和,并判断当访问到叶子节点的时候,如果和不等于期待值,递归函数自动返回到上一个节点,这个时候需要在返回之前在容器中删除当前节点。容器实际上就是一个栈,因为深度遍历到叶子节点,才知道这个路径符不符合。
————————————————————————————————————————————
问题:如何把一个二叉树转换成为一个双向链表
解决:利用二叉树的中序遍历,二叉树的中序遍历是将二叉树从小到大遍历一次,那么对于一个节点,指向左节点就作为链表的前驱,又节点就作为链表的后驱,用这种方式可以完成二叉树到双向链表的转移
————————————————————————————————————————————
问题一:二叉树的深度
解决:这个问题比较简单,利用递归去看左子树的深度和右子树的深度,取最大值+1就是整个树的深度,一直递归到叶子节点。
问题2:输入一颗二叉树的根节点,判断该树是不是一个平衡二叉树
解决:
这个问题很明显用到之前问题一的解决方法,既然知道每个节点的额深度,那么判断每一个节点的子节点的深度不超过1,就是平衡二叉树
但是这种方法,显然求深度的遍历到叶子节点,比较的时候每次都要求深度,遍历叶子节点,效率不高,这种时候就需要从下到上的方法判断,避免不要的重复遍历,
思想就是用后序遍历遍历一个节点,每次遍历节点的时候就判断左右子树的深度值,直到最后的根节点即可。
————————————————————————————————————————————
题目:给定一颗二叉搜索树,请找出其中的第k大的结点。例如, 5 / \ 3 7 /\ /\ 2 4 6 8 中,按结点数值大小顺序第三个结点的值为4。
解决:显然,对二叉树进行中序遍历,中序遍历节点k位置即为第k个节点,进行中序遍历的时候,对遍历的节点计数,计数值满足的时候,直接退出就可以了
————————————————————————————————————————————
问题:请实现两个函数,分别用来序列化和反序列化二叉树
解决:拿到这个问题,首先想到什么是序列化,反序列化,仔细一想,原来这就是将一个序列变成一个二叉树,反过来,将一个二叉树转换成为一个序列
将一个二叉树变成一个序列,首先得需要将这个序列存在在一个容器中,然后对二叉树进行一个前序遍历即可,通过递归就可以完成。
将一个序列变成一个二叉树,首先按照序列,创建二叉树的节点,然后按照序列的顺序,按照前序遍历,根左右来创建一个二叉树
————————————————————————————————————————————
问题:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
解决:拿到问题,一看这就是一个二叉树的层次打印,首先遍历的节点,先打印,那么这就是一个先入先出的现象,适合用队列来实现。当某个节点进入队列,然后遍历是否存在左右节点,存在左右节点则进入队列,出队列的时候对这个队列的所有的元素全部出队列,并且打印,一次出队列就是一行。
————————————————————————————————————————————
问题:请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解决:这个很明显还是一个层次打印的问题,用容器保存每一行的,然后不断反转容器,打印即可。
————————————————————————————————————————————
问题:请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
解决:拿到这个问题,首先想到的是镜像二叉树的判断和相同二叉树的判断,我们通过一个前序遍历,然后对某个节点右左右两个子树是否相同,那么就要判断,左子树的左子树和右子树的右子树是不是相等,同时判断左子树的右子树和右子树的左子树是不是相等。
————————————————————————————————————————————
面试题:二叉树的下一个节点
问题:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
解决:这个问题拿到手,首先想的是二叉树的下一个节点,中序遍历,那么有可能从左子树返回到根节点上,所以首先要画图,搞清楚有哪几种情况
链接:https://www.nowcoder.com/questionTerminal/9023a0c988684a53960365b889ceaf5e
结合图,我们可发现分成两大类:1、有右子树的,那么下个结点就是右子树最左边的点;(eg:D,B,E,A,C,G) 2、没有右子树的,也可以分成两类,a)是父节点左孩子(eg:N,I,L) ,那么父节点就是下一个节点 ; b)是父节点的右孩子(eg:H,J,K,M)找他的父节点的父节点的父节点…直到当前结点是其父节点的左孩子位置。如果没有eg:M,那么他就是尾节点。
按照三种情况,第一种,有右子树,那么下个节点就是右子树最左边的点
第二种,没有右子树,本身是父节点的左孩纸,下一个节点就是父节点
第三种,没有右子树,本身是父节点的右孩纸,那么他的父节点的父节点的父节点…直到当前结点是其父节点的左孩子位置,如果没有就是尾节点
————————————————————————————————————————————
问题:如何查找一个旋转数组中的最小值,旋转数组指的是已经排好序的数组,将前面的n位数字旋转到后面n位。
解决:
方法1:很直观的想到,遍历整个数组,找到最小的值,这样的时间效率就是O(n),这是很明显的 ,但是显然这种方法不对,因为题目已经说明了是旋转数组,这种方法不管是不是旋转的数组,都这样做,显然不是出题者的考察的目的。
方法2:利用旋转数组,部分排序好的特点,进行二分查找。通过二分中间元素和开始末尾元素进行比较,判断属于前区间还是后区间,然后二分的缩短区间,当缩短区间到1的时候,那么后面的元素就是最小值了。
如下:
注意点:
需要考虑两种特殊情况:
情况1:如果选择0个元素的数组,其实就是排序好的,这个时候对二分算法产生的影响需要考虑
解决:这种情况应该首先判断首元素和尾元素的大小,如果尾元素还要大的话,那证明已经排好序了,这个时候是不要进行二分的,直接输出首元素即为最小值
情况2:如果选择之后,头尾元素中间元素都是相同的,这个时候,无法通过大小来判断中间元素属于哪个区间,这个时候怎么办,二分的办法似乎失效了?
解决:这种时候只能采用顺序查找了,因为如果都相等,无法通过二分缩小区间,只能顺序的比较查找了。
————————————————————————————————————————————
问题:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
思路:这个题目,拿到手,首先想到这就是一个topk的问题,那么首先可以不排序使用类似快排的思想,如果是大数据量,可以想到用外部排序的方法实现,但是仔细思考这个问题又不是一个topk的问题,为什么这么说呢?因为你根本就不知道k,只知道是数据流的中位数,如何做呢?
利用大小堆,建立两个堆,一个大堆一个小堆,大堆始终小于小堆,保证两个堆的深度相差1,那么中位数一定就是在大堆的堆顶,或者是小堆的堆顶。
具体的堆我们可以用stl中的优先队列来实现
class Solution {
//建立一个最大推和一个最小堆
private:
priority_queue,less> p;
priority_queue,greater> q;
public:
void Insert(int num)
{
if(p.empty() || num <= p.top()){
p.push(num);
}
else{
q.push(num);
}
//如果最小堆的元素比最大推的大,那么将最小堆的元素放到最大堆上
if(p.size() == q.size()+2)
{
q.push(p.top());
p.pop();
}
if(p.size()+1 == q.size())
{
p.push(q.top());
q.pop();
}
}
double GetMedian()
{
if(p.size() == q.size())
return (p.top()+q.top())/2.0;
else
return p.top();
}
};
————————————————————————————————————————————
问题:斐波拉契算法的使用,几种实现方法的比较?
解决:
例如:
青蛙跳台阶的问题(http://blog.csdn.net/imsenior/article/details/53207788)
矩形覆盖问题(http://blog.csdn.net/sbq63683210/article/details/52071387)
斐波拉契数列的公式:
F[n]=F[n−1]+F[n−2]
按照公式直接递归的使用:
缺点:
和其他的普通递归的缺点一样,需要进行大量的压栈处理,保留上次的堆栈信息,导致程序运行很慢。
解决普通递归压栈的问题,通过增加额外的输入变量,来完成尾递归的处理。
缺点:
需要依赖编译器做处理,但是这种尾递归一般编译器都会做堆栈覆盖处理,所以这种方法实际应用最好
从0 1 还是不停的迭代到n,计算最终的数据
缺点:
代码写起来较为复杂。
普通的递归是从顶到底计算的思想,而尾递归和迭代都是从底到顶的思想进行的。
————————————————————————————————————————————
问题:输入一个整型数组,数组里面有正数和负数,求连续子数组的和的最大值
解决:这个问题常见,之前看算导论的时候也有遇到过,主要有以下三个解决思路:
1、暴力破解:通过两个循环,第一个循环确定子数组的首地点,第二个循环向后遍历。
时间复杂度O(n^2)
2、分治法:分而治,分治法的常见策略是将其分为两个相等的区间。所以这个问题就划分为子数组区间三种情况,一种在左边区间,一种在右边区间,一种横跨左右两个区间。
对于左边区间,右边区间,我们可以用递归解决,类似归并排序的思想,得到的复杂度就是O(nlogn)
对于横跨区间,也比较好解决,因为必须有中间点,所以只用遍历一遍就知道了。复杂度是O(n)
最后的复杂度就是O(n+nlogn)=O(nlogn)
3、最为简单,就是动态规划的思想,动态规划之前算导里面有了解过,要使用的两个条件就是重复的子问题,最优的子结构,思想就是写出递归表达式来解决。
这个问题用动态规划来解决,转换为两个:求第i个位置子数组的最大值,只有两种情况,第一种就是前面I-1个最大子数组和小于0,那么最大子数组就是当前子数组,如果前面I-1个最大子数组和大于0,那么第i个位置最大子数组就是前面I-1个最大子数组和+当前子数组值。
这种方法,只需要遍历一遍数组即可,这样的时间复杂度只需要O(n)
————————————————————————————————————————————
问题:找到从小到大的顺序的第1500个数
解决:
这个问题首先很直白的想法很简单,从1开始对每个数字不断的取余2,3,5.判断是不是丑数,给一个计数值,记录当前丑数的个数,直到计数值等于1500,即为第1500个数。
缺点:这个方法是可以,但是很明显,它对所有的数据都进行取余比较,那么不是丑数的也被计算了一遍,显然增大了开销。有没有方法可以不去对不是丑数的数进行操作,只针对丑数??
有,从0开始自己生成丑数,并记录把所有生成的丑数都放在一个数组中,这样通过前面的丑数不断的生成后面的丑数,当生成到1500个即可。
————————————————————————————————————————————
问题:n个连续的数字排成一圈,从0开始每次从这个圆圈中删除第m个数字,求这个圆圈剩下的最后一个数字
解决:
这个问题,最经典的解决方法就是循环链表,用一个循环链表去模拟这个圆圈,然后依次进行遍历删除即可,但是这种方法时间复杂度高,需要多次遍历,o(nm)
更好的办法,利用数学公式找到递归的关系:
————————————————————————————————————————————
问题:位操作,输入一个数,判断二进制中1的个数。
解决:问题比较简单,很直观的可以想到是用位操作的移位运算符来完成。
输入的数,先判断末尾是不是1,然后使用向右移动操作符,在判断是否为1,直到移位为0
==问题:==对于二进制数的向右移位操作,正数左边补0,负数左边补1,故这种方法当遇到了输入是负数的时候,此时会导致死循环。
将数据1不断的左移,依次和输入的数据相与,直到左移到0.
==问题:==左移的次数较多,循环次数等于机器操作数,即32/64位
使用减法相与的方法进行判断。
原数-1在与原数相与,得到的结果相当于把二进制最右边的一个1变成0;
例如,输入1100 ,1100-1=1011,1011&1100=1000,即判断了第一个1.
1000-1=0111,0111&1000=0;即结束,判断有两个1.
这种方法较好,程序简单,不会有死循环,效率也较高,但是也最难想到。
总结:考察位运算相关变形的题目
1.用一条语句判断一个整数是不是2的整数次方。
思路:2的整数次方,二进制中只有一位是1,其他为是0,即转换为二进制中1的个数;
2.输入m,n,计算需要从m的二进制改变多少位转换为n。
思路:两个数异或,得到需要改变的位,这些位有多少个1即为需要改变多少位。
总的来说,和位运算相关的记住这个法则:把一个整数减去1之后再和原来的整数做位运算,得到的结果相当于是把整数的二进制位中最右边的一个1变成0;
————————————————————————————————————————————
问题:求数值的整数次方
解决:问题很简单,但是需要考虑很多边界条件,和负面的条件,这也是这个题目的真实考察点。
首先考虑两种不同的情况:
情况1:负面测试,0是没有负指数乘方的
情况2:指数为负,需要求其负倒数
情况3:任何数的0次幂都是1,不包括0。
代码中对三种情况进行边界处理:
情况1处理:
情况2处理:
情况3处理:
综合这三种情况,即可写出完成的代码:
因为乘方完全可以进行快速迭代,即不需要一次次的相乘原来数据。
可以采用类似斐波拉契的迭代推导式,分为奇数偶数,优化乘方的过程
注意递归运算的时候,递归的结束条件是0次幂返回1,1次幂返回其本身。由于只是优化的乘法迭代的过程,所以边界条件的保护都是由上次函数保证的。
————————————————————————————————————————————
问题:输入一个数组,从外向里以顺时针依次按照顺序打印出每个数字
解决:遇到这种题目,首先必须得进行画图说明,画图才是王道,画图看下顺序打印的方法,不同的类型。
首先需要判断的是打印需要几次循环,通过枚举来看规律,其次判断循环每次打印是否需要从左右上下都打印,还是由不同的判断条件,确定好了这些进行编码即可。
————————————————————————————————————————————
问题一:查找一个递增数组中,两个数的和是s的可能性由多少种。
解决:这个问题既然说了是递增数组,那么肯定就是有序的,有序的通过两个指针遍历,就可以知道有多少中国可能性的
问题2:打印所有和为s的连续正整数
解决:这个问题需要用实际的分析一下,其实也是两个指针递增的问题,和s进行比较,如果大了小指针增大,小了大指针增加。
————————————————————————————————————————————
问题:求n个骰子和为s的所有可能值出现的概率
解决:
首先这个问题拿到手的时候,想着就是一个递归的问题,为什么这么说呢,由于一个筛子的和为1-6种可能性,那么n个筛子的和为s的组合就把有n-1个筛子为s-1,s-2,s-3,s-4,s-5,s-6.也就是我们需要求得n-1个筛子和为s-1,s-2,s-3,s-4,s-5,s-6,把结果相加就是n个筛子和为s的次数。
我们用一个数组取保存所有点数出现的次数,最后除以总可能性,求概率即可。
————————————————————————————————————————————
问题:从扑克牌中随机抽取5张牌,判断是不是一个顺子?
解决:这个问题很简单,就是需要用数字去模拟,首先对取的数据进行一个排序,然后数字之间的空位有多少个,然后看有没有大王小王,个数是否大于空位,大于空位则是癞子,可以完成填充一个顺子。
————————————————————————————————————————————
问题:求1+2+…n,不能使用乘除法,循环,if等语句
解决:很明显这个问题就是要考察能不能使用其他的方法完成常规的递归和循环。
主要可以用以下几个方式解决:
1.利用构造函数
2,利用虚函数
3.利用函数指针
4.利用模板类
————————————————————————————————————————————
问题:求两个数的和,不能用加减乘数
解决:这个问题拿到手,首先想到的就是要利用位操作来完成加法的功能
————————————————————————————————————————————
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
思路:
分析:回溯算法
这是一个可以用回朔法解决的典型题。首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的
第i个位置。如果路径上的第i个字符正好是ch,那么往相邻的格子寻找路径上的第i+1个字符。除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。
重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。
由于回朔法的递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个
字符,这个时候只要在路径上回到第n-1个字符,重新定位第n个字符。
由于路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row,col)的
格子和路径字符串中相应的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符
==如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。==一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置 ==
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, char* str)
{
if(str==NULL||rows<=0||cols<=0)
return false;
//用一个数组保存走过的路径
bool *isOk=new bool[rows*cols]();
//遍历整个矩阵,判断是不是路径
for(int i=0;i=rows)
return false;
//判断当前值是不是字符串需要的值
if(isOk[curx*cols+cury]||*str!=matrix[curx*cols+cury])
return false;
//遍历过当前值,置0
isOk[curx*cols+cury]=true;
bool sign=isHsaPath(matrix,rows,cols,str+1,isOk,curx-1,cury)
||isHsaPath(matrix,rows,cols,str+1,isOk,curx+1,cury)
||isHsaPath(matrix,rows,cols,str+1,isOk,curx,cury-1)
||isHsaPath(matrix,rows,cols,str+1,isOk,curx,cury+1);
//如果找了一圈都没有命中,则置无效
isOk[curx*cols+cury]=false;
return sign;
}
};
————————————————————————————————————————————
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
思路:
1.从(0,0)开始走,每成功走一步标记当前位置为true,然后从当前位置往四个方向探索,
返回1 + 4 个方向的探索值之和。
2.探索时,判断当前节点是否可达的标准为:
1)当前节点在矩阵内;
2)当前节点未被访问过;
3)当前节点满足limit限制。
public int movingCount(int threshold, int rows, int cols) {
boolean[][] visited = new boolean[rows][cols];
return countingSteps(threshold,rows,cols,0,0,visited);
}
public int countingSteps(int limit,int rows,int cols,int r,int c,boolean[][] visited){
if (r < 0 || r >= rows || c < 0 || c >= cols
|| visited[r][c] || bitSum(r) + bitSum(c) > limit) return 0;
visited[r][c] = true;
return countingSteps(limit,rows,cols,r - 1,c,visited)
+ countingSteps(limit,rows,cols,r,c - 1,visited)
+ countingSteps(limit,rows,cols,r + 1,c,visited)
+ countingSteps(limit,rows,cols,r,c + 1,visited)
+ 1;
}
public int bitSum(int t){
int count = 0;
while (t != 0){
count += t % 10;
t /= 10;
}
return count;
}
————————————————————————————————————————————
有几个题很相似,但是解决的方法不同:
例如:海量数据中找第k个大的数——类排序,外部排序
求数据流的中位数——类排序,最大堆最小堆
树的层次遍历
树的镜像判断
树的相同判断
链表环的寻找
链表倒数k的节点的选择
寻找非重复的字符——分治+异或
寻找第一个非重复的数组——队列+次数表
寻找第一个重复的字符——bitmap表
字符串的排序,去重——bitmap表
两个队列转换成为一个栈
两个栈转换成一个队列
栈最小值操作
最大子数组
斐波拉契
丑数
圆圈删除数字
n个鸡蛋从m层楼上扔下来,确定k次即可辨别零界点
http://blog.csdn.net/wolinxuebin/article/details/47057707
给50个硬币,面值可以不同,排成一排,两个人轮流取,只能从两端取,先取的人如何保证取到的币值大于等于另一个人。
f[i][j]=max(num[i] + sum[i+1][j] - f[i+1][j] , num[j] + sum[i][j-1] - f[i][j-1])
f[i][j]表示从i到j取的最大和
sum[i][j]表示从i到j的和
迷宫
数字寻找路线