《剑指offer》算法总结

最近找工作,刷了下《剑指offer》这本书,上面有挺多经典的算法,对每一章做了一个总结,章节笔记见一下链接:

http://note.youdao.com/noteshare?id=ad655fcf73eb4b63e7e0700daa01d02f

下面是对整本书的一个算法,从数据结构上进行分类,这样拿到算法题的时候知道从哪一个类别去思考相似的可能性。

一、线性表
1、数组
2、链表
二、字符串
三、栈和队列
四、树
五、查找和排序
六、动态规划
七、细节实现题
八、回溯法


一、线性表

1、数组

面试题3:二维数组中的查找
知识点5:

对于一个有序的二维数组的搜索,如果二维数组每一行都从左到右递增排序,每一列都从上到下递增排序,如果快速的搜索到数组中的元素???

解决方案:

从数组的左下角或者右上角进行搜索,通过大小判断去除当前行或者列,缩小搜索的二维数组的大小,最后确定搜到元素。

————————————————————————————————————————————

面试题14:调整数组顺序使得奇数位于偶数前面
知识点4:考虑扩展性的解放,函数指针的应用

问题:输入数据,调整数组的顺序,奇数在前,偶数在后

解决:这个问题看起来很简单,最直观想到的方法就是从前往后遍历,偶数则提出来,放到最后,其余数据向前移动,直到遍历到末尾,这样的时间复杂度是O(n^2).
那么有没有改进的办法呢???这一来移动的问题,改进就是用空间来换取时间,方法就是交换。

设置两个指针,一个从前往后,一个从后往前,前指针找偶数,后指针找奇数,碰到奇偶则交换,指针碰头则便是重排序完成。

这种方法很好了,但是如果出题者还想考察扩展性,这个时候应该怎么办呢?出题者认为排序的方法是可以变动的,现在是奇偶,后面可能是正负等,应该如何做呢????
这就用到了设计模式的一种,将变动的提取出来, 即将重排序的方法写成一个函数。每次比较就调用这个函数就可以了,注意使用的是函数指针。

值得注意的是函数指针的用法,用函数指针来代替函数,则每次实际执行的是函数指针,那么函数对外传参就是一个指针,我们只需要将这个指针指向不同的函数即可,不需要改变函数内部的代码,函数指针是c语言里面用来封装变化量。
————————————————————————————————————————————

面试题29:数组中出现超过一半的数字
知识点1:

问题:判断数组中出现次数超过一半的数字

解决

这个问题拿到手,看起来简单,遍历就可以,但是显然这样的时间复杂度是不符合出题者考察的,需要改进。

改进方法1:

仔细思考,数组中出现次数超过一半的数字,那么排序后,这个数字肯定是在数组的中间,也就是中位数,所以这个问题就转为求数组中中位数,数组中任意第K大的数字,之前有提过,可以利用快排的思想去解决。

方法2:

利用计数,遍历一遍,相同的数字+1,不同的数字-1,归零的时候记录下一个数字,并置计数器为1.

————————————————————————————————————————————

面试题30:最小的k个数

问题:求数组中最小的k个数

解决:首先想到的还是快排的思想,找最小的k个数,左边的区间即可,这样的复杂度页很低o(n),唯一的缺点是会改变数组的内容,可能不太需要。

第二个方法,就是基于辅助空间桶来达到降低复杂度的目的,桶存储k个数据,这k个数据在桶里面存储只要保证查找,删除,增加的时间复杂度是lgk,那么对于n个数找到最小的k个数的复杂度就可以降为nlgk.
那么这种存储方法如何选择呢?选最大值肯定想到是最大堆,然后就是树。可以用STL去完成

————————————————————————————————————————————

面试题33:把数组排成最小的数

问题:把数组排列成最小的数

解决:这个问题,首先相当的是全排列,给出每一种全排列,然后依次比较每个全排列即可,但是这种方式的n个数字就是n!的数,显然不是面试官想要的方法。
更好的方式,是直接通过排序规则,把数组排列成最小的数。

例如,数组为{3,32,321},那么我们最后需要排列的是{321,32,3},然后将它连接起来输出即可。很明显我们需要做的是自己定义个比较的规则,这三个数如何比较得到大小,按照位数进行比较,给出比较规则,直接利用数组的排序即可解决

————————————————————————————————————————————

面试题36:数组中的逆序对

知识点7:
问题:求数组中的逆序对

解决:这个问题看起来非常的麻烦,因为要求数组中逆序对,那么首先想到的就是遍历,每遍历到一个数字的时候,就对它的后面的数字进行比较,找逆序对,这样的时间复杂度肯定就是o(n2)

那么有没有什么方法可以减少复杂度呢??

有:方法就是归并法
首先将数组进行划分,划分为两个子数组,那么如果这两个子数组自身的逆序对知道的情况下,我们只需要对这个两个子数组进行逆序对的获取即可,不用管他们本身的逆序对,按照这个思想,不断的划分 ,知道最后单个数字,这样做比较的次数就是归并排序的复杂度。

————————————————————————————————————————————

面试题40:数字在排序数组中出现的次数

问题:数组中只出现一次的数字

解决:这个问题很巧妙,为什么这么说呢?如果一个数组中只有一个数字出现一次,其余都是两次,那么根据异或的思想,相同的数字都会被置0,最后只会得到一次的数字。

如果有两个数字只出现一次呢???
那么我们需要对这个数字进行一个分组,分组的依据就是根据异或的结果中为1的值,进行寻找分组。


————————————————————————————————————————————

面试题51:数组中重复的数字

问题:数字在排序数组中出现的次数

解决:只要是排序数组,首先想到的就是二分法,因为排序二分法的效率就会非常的高,如何才能统计出现的次数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)
————————————————————————————————————————————

2、链表

面试题5:从尾到头打印链表

问题:单向链表的反向输出,即从尾到头的输出。

解决:

方法一:

由于单向链表,给予都是头指针,头指针从头到尾依次输出,所以想到的就是遍历就是从头到尾,输出从尾到头,这就是先进后出的规则,典型的栈啊!!!所以用栈去做一个遍历的保存节点,再依次从出栈输出即可:
在这里插入图片描述

方法二:

既然想到了用栈来实现这方法,那么递归肯定也能实现,应该递归结构从本质上来说就是栈结构,所以可以用递归来实现,如果输出某个节点,先递归输出它后面的节点即可:

问题:
这样做代码简单,的确很好,但是存在的问题就是这种方法并不是尾递归,如果链表非常长的时候,会导致函数调用的层级很深,从而导致函数的调用栈溢出。

关于递归和尾递归的区别;

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

尾递归的原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
http://blog.csdn.net/zcyzsy/article/details/77151709
————————————————————————————————————————————

面试题13:在O(1)时间删除链表结点

问题:给链表的头结点,待删除节点,进行删除节点的操作:

解决:
分析这个问题,看起来很简单,就是链表的删除操作,从头结点遍历到待删除节点的前一个节点,将前节点下一指针指向删除节点的后节点,然后删除待删除节点即可。这种方法显而易见,需要考虑出题人是不是为了考虑这个,显然不是,因为遍历这种方法的复杂度肯定是O(n),而出题者的意图肯定是想办法去减少这种删除操作的复杂度。

方法:仔细分析,为什么要遍历,因为单向链表我们找不到删除节点的前驱节点,所以从头结点开始遍历才能找到,但是我们可以换一个思维去想,删除节点不一定要删除当前节点,可以从后一个节点覆盖前面的节点,这样当前删除节点被覆盖也是被删除了,然后将后一个覆盖的节点就删除了。

总体的思想:先找到待删除节点的后一节点,从后一节点覆盖待删除节点;将待删除节点指向后一节点的后一节点;删除后一节点,即可。
这种方法没有进行遍历,所以时间复杂度是O(1)。

但是这种方法有两种情况需要考虑。
第一种如果是尾节点删除,此方法不能执行,还是按照原来的遍历删除。
第二种是头节点删除,则可以采用此方法,但是需要把头结点指针重新指向到当前的头结点上。

  • 1
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值