数据结构与算法之美(专栏)

复杂度分析

时间复杂度

2018/12/21 posted in  算法

OFFER 第2章 基础知识

3. 数组中重复的数字

3.1 不修改数组找出重复数字

4. 二维数组中的查找

5. 替换空格

6. 从尾到头打印链表

7. 重建二叉树

8. 二叉树的下一个节点

9.用两个栈实现队列

10. 斐波那契数列

10.1 青蛙跳台阶问题

11. 旋转数组中的最小数字

12.矩阵中的路径

13.机器人的运动范围

14. 剪绳子

15. 二进制中1的个数

2018/10/1 posted in  算法

OFFER 第6章

53. 在排序数组中查找数字

统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2,3,3,3,3,4,5}和数字3。由于3在这个数组中出现了4次。因此输出4;

54. 二叉搜索树的第k大节点

给定一颗叉树的搜索树,请找出第k大的节点。

中序遍历就可以了

55. 二叉树的深度

输入一颗二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点形成树的一条路径,最长路径的长度为树的深度。

55.1 平衡而叉树

输入一颗二叉树的根节点,判定该树是不是平衡二叉树。如果某二叉树中任意节点的左,右子树的深度相差不超过1,那么它就是一颗平衡二叉树。

56. 数组中数字出现的次数

56.1 数组中只出现一次的两个数字

一个整型数组里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度时O(n),空间复杂度是O(1);例如{2,4,3,6,3,2,5,5}

56.2 数组中唯一只出现一次的数字

在一个数组中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

57. 和为s的数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s,若果有多对数字的和等于s,则输出任意一对即可。

思路1
一个一个遍历相加试探。时间复杂度o()n2

思路2
一个个遍历,后面的有序的进行二分查找s - c的数。时间复杂度o(n*logn)

思路3
采用两个指针,头指针,尾指针,相加大于s时,尾指针向前移动。小于s将头指针向后移动。知道找到为止。时间复杂度O(n)

57.1 和为s的连续正数序列

输入一个正数s,打印出所有和为s的连续正数序列。例如输入15,由于1+2+3+4+5 = 4+5+6=7+8 = 15,所以打印出3个连续序列1~5,4~6,和7~8;

思路
从头开始,逐个比较,直到只有两个相加大于s为止。时间复杂度是O(s2);

优化思路
用两个指针,头,尾,尾向后移动大于s时,去掉头,小于s时,增加尾。直到位编程s/2为止。

58. 翻转字符串

58.1 翻转单词顺序

输入一个英语句子,翻转句子中单词的顺序,但单词内字符顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student",则输出"student. a am i".

思路
先将每个单词翻转,在将整个翻转。整个在与代码的实现。

59. 队列的最大值

59.1 滑动窗口的最大值

给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。
例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为{4,4,6,6,6,5}。

思路
从上面来看,滑动窗口就是一个队列。我们可以用一个双端的队列(即可以从头部出,也可以尾部弹出)。在向后移动时,要加入的元素比前一个元素的值大时,将前面的元素移除。将后面的元素加入。如果要小,不将后面的值放入队列。每次比较时候,都要判定,头部元素是否还在窗口的范围里。

59.2 队列的最大值

请定义一个队列并实现函数max得到队列里的最大值,要求函数max,push_back和pop_front的时间复杂度都是o(1)

思路
和上面的思路一样

60. n个骰子的点数

把n个骰子扔在地面上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

61. 扑克牌

2018/9/28 posted in  算法

OFFER 第4章 解决问题的思路与方法

27.二叉树的镜像

请完成一个函数,输入一颗二叉树,该函数输出它的镜像。

思路
遍历时,交换左右子树。然后递归的去交换子树就可以了。直到root为null

28.对称的二叉树

请实现一个函数,用来判断一颗二叉树是不是对称的。如果一颗二叉树和他的镜像一样,那么它是对称的。(LeetCode 101. Symmetric Tree)

思路
用前序遍历时时“根左右”,通过观察与“根右左”的在对称的二叉树序列是一样的。哈哈。在定义三种遍历方式时,都是针对于根的。没有定义左于右的顺序。
不过这个递归表达是由点难想到。

29. 顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。例如输入如下矩阵
Xnip2018-10-01_22-21-50

思路
可以将矩阵分圈打印。那么难点在于边界条件的判定。

30. 包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数。在该栈中调用min,push及pop的时间复杂度O(1)

思路
入栈比较,是最小值入栈,不是最小值,将栈重新入栈。这样每出栈时,辅助栈也会出栈。

31. 栈的压入与弹出序列(这个思路是清晰的,但实现起来有点麻烦未实现完)

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列{1,2,3,4,5}是某栈的压栈序列,序列{4,5,3,2,1}是该压栈序列对应的一个弹出序列,但{4,3,5,1,2}就不可能是该压栈序列的弹出序列。

思考
可以模拟入栈与出栈的过程,待输入序列是1,2,3,4,5,待输出序列是4,5,3,2,1,则,对入栈一直入栈,当发现即将要入栈的元素与输出序列的元素相等时,入栈并出栈这个元素。头部元素与输出序列的下一个比较,相等则出栈。不相等就将下一元素入栈。发现头部元素与待书序序列不等,且与即将输入的元素不等时,即不是其输出序列。

32. 从上到下打印二叉树(不换行)

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。例如,输入图4.6中的二叉树,则依次打印出8,6,10,5,7,9,11。

思路(我的)
就是二叉树的层序遍历。
层序遍历的实现,根入队列,出队列时将其左,右节点入队列。知道最终队列为空。

32.1 从上到下打印二叉树(换行)

从上到下打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

思路
与上面的相同,只是需要找到如何去标记换行的问题。
换行的问题,可以记录入已经在本行打印了的,记录下一行要打印的个数。

32.2 之字形打印二叉树

实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行在按照从左到右的顺序打印,其他行以此类推。

思路(哈哈,这个是我自己想出来的哟)

和上面的思路类似。只是为了保证打印时的顺序按照所需要的。搞两个栈,一个正序入栈,一个逆序入栈。相互交换着输出。

33. 二叉搜索树的后序遍历序列

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回true,否则返回false.假设输入的数组的任意两个数字都互不相同。例如,输入数组{5,7,6,9,11,10,8},则返回true,因为这个整数序列是图4.9二叉搜索树的后序遍历结果。如果输入的数组时{7,4,6,5},则由于没有哪棵二叉搜索树的后序遍历结果是这个序列,因此返回fasle;

思路(我自己分析出来的)
思考过程,刚开始的确没有思路,从二叉搜索树的特征来看,每个节点的左边小于根节点,每个节点的右边大于根节点。所以可以找到整个序列的根节点。然后找出左子树与右子树。在分别对左右子树看其是否左子树元素都小于根,右子树都大于根。

测试用例
完全二叉树,只有左子树,只有右子树,只有一个节点,空树等。

34. 二叉树中和为某一值的路径

输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

思路
要寻找某一条路径。明显的找到根节点然后尝试添加为和。知道根节点。在外部维持一个栈,访问元素时,添加栈,退出递归时将元素移除栈。在刚好和的值与要找的相等时,刚好栈里就是路径。

测试用例
如果有多个路径能支持的情况
在中间值,就已经找到了
在叶子节点才找到

35.复杂链表的复制(这题要有发散思维)

请实现函数复制复杂链表。在复杂链表中,每个节点除了有一个m_pNext指针指向下一个节点,还有一个m_pSibling指针指向链表中的任意节点或者null;

思路
我的思考,先将主要的链条连接起来。然后将次要链条连接。那么思考在次要链表的链接上实际可能的花废O(n2);

优化思路1
基于以上思路,o(n2)的花费主要是寻找其他节点位置上。可以用hasn表存储的对应关系。当原始是N时复制的链表是N`.
这样解决的办法是用空间换时间。

优化思路2(这种)
对于优化思路1的空间问题,我们要实现不用辅助空间的情况下实现O(n);
Xnip2018-09-27_15-58-18
Xnip2018-09-27_15-58-31

36. 二叉搜索树和双向链表

输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的节点,只能调整树中节点指针的方向。

思路
因为二叉的特性。根节点一定大于左子树的所有节点,根节点一定小于右子树的所有节点。要将其换成双向链表。首先弄成单向链表。将子树的右子树移动到根节点的右上。左子树移动到父节点的右下。递归进行。

37. 序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树

思考
二叉树的序列化,就是保证序列化后的唯一。要保证唯一。可以保存先序遍历,在保存中序遍历。这样可以恢复二叉树。

优化思路(这种思路较好,用特殊字符代表null,是我没想到的)
上面的过程需要将两个序列化都读取后才能进行恢复。我们可以根据前序遍历的顺序来序列化二叉树,因为前序遍历是从根节点开始的。在遍历二叉树碰到null指针时,这些null指针序列化为一个特殊的字符。

38.字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出字符a,b,c所能排列出来的所有字符串,abc,acb,bca,cab,cba;

思路(思想很重要)
通过上面的排列,可以将其分成以a开头的,以b开头的,以c开头的,然后分别后面的进行排列,重复这个过程。
即将问题转换成子问题。同过子问题的递归。

2018/9/28 posted in  算法

OFFER 第5章 优化时间效率与空间效率

39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半。请找出这个数字。例如,输入一个长度为9的数组{1,2,3,2,2,2,2,5,4,2}。由于数字2在数组中出现了5次。超过数组长度的一半。因此输出2;

40. 最小的k个数

输入n个整数,找出其中最小的k个数,例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4;

41. 数据流中的中位数

如何得到一个数据流中的中位数,如果从数据流中读取奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值。那么中位数就是所有数值排序之后中间两个数的平均值。

42. 连续子数组的最大和

输入一个整型数组,数组里有正数也有负数。数组中的一个或者连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n);

43. 1~n整数中1出现的次数

输入一个整数n,求1~n这n个整数的十进制表示1出现的次数。例如输入12,1~12这些整数包含1的数字有1,10,11和12,1一共出现的次数为5次。

44. 数字序列中某一位的数字

数字以012345678910111213141516的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位1,第19位时4等等。请写一个函数,求任意第n位对应的数字。

思考
一般的解决思路是不断累加数字,直到找到第几位。
我自己的优化思路,比如数字967,(967-99)*3 + (99-9)*2+ 9*1

45. 把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接处的所有数字中最小的一个。例如输入数组{3,2,321},则打印出这3个数字能排成的最小数字321323;

思考
直接的做法是求全排列进行比较。
另一种做法是要将数字最小的放在其前面,在首字相同,又要比较第二个的大小。这样就可以联想到,通过自定义一种比较规则,然后通过nlog(n)的时间里。将其排序出来,那么这个规则的定义与证明就是一个数学问题。

46. 把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0翻译成a,1翻译成b,25翻译成z,一个数字可能有多少个翻译。12258有5种不同的翻译,分别是bccfi,bwfi,bczi,mcfi,mzi,请编程实现以函数,用来计算一个数字有多少中不同的翻译方法。

思路
没想明白

47. 礼物的最大价值

在一个m*n的棋盘的每一格都放右一个礼物,每个礼物都有一定的价值。你可以从棋盘的左上角开始拿格子里的礼物,并每次向左或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘及其上面的礼物,请计算你最多能拿到多少价值的礼物?

思考
我当前的结果。如何由前面的结果得来。而分治法是由一些子问题,在综合成大问题。
动态规划有时是由向大。因为基于原有小的已经算出来的值,取计算大规模的值。
这里当前的最大值,从上,从左而来。我需要比较从上的最大值+当前值,与从左而来的值+当前值 两个比较。以求出当前的最大值。也就是利用以前的结果计算当前的结果。

48. 最长不含重复字符的子字符串

从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含‘a'~'z' 的字符。例如 在字符串’arabcacfr‘中,最长的不含重复字符的子字符串’acfr',长度为4;

思考
后面的依赖前面结果,很容易想到动态规划。
需要注意的是,动态规划的实现,可以考虑使用循环来实现。而不是使用递归。

49. 丑数(一点思路都没有)

我们把只包含因子2,3,5的数叫做丑数。求按从小到大的顺序的第1500个丑数。例如6,8都是丑数,但14不是,因为它只包含因子7,

50.第一个只出现一次的字符

字符串中第一个只出现一次的字符。在字符串中找出第一个只出现一次的字符。如输入abaccdeff,则只输出b;

思考
一种思路是从头开始然后遍历到最后,知道找到只有一次的字符。时间复杂度是O(N2)

另一种思路
遍历一次保存字符的次数。用hash表,以字符为key值,以出现的次数为值。最后再遍历一次hash表,就可以找出该首先出现只有一次的字符。时间复杂度是O(n)+O(n),即最终是O(n)。

51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对,输入一个数组,求出这个数组中的逆序对的总数。例如在数组{7,5,6,4}中,一共存在5个逆序对,分别(7,6),(7,5),(7,4),(6,4)和(5,4);

思路(这个题我没有比较清晰的思路)

52. 两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点。
Xnip2018-09-28_18-10-12

思路一
从一个链表开始遍历,比较第二个链表。知道找到相同值即可。时间复杂度时O(N2)

思路二
假如我们从后面往前面遍历就好了。这样找到最后的不同就可以了。
我们想到,用两个辅助的栈,每个链表加到各自的栈里,然后同时出栈就可以直到最后一个相同为止。时间复杂度是O(N),空间复杂度也是O(N)

思路三
遍历链表一,几下总数,遍历链表二,几下总数,用长的向前走动两个节点数之差。然后同时相前走动,直到相同为止。这种是最好的思路。

总结

降低时间复杂度的方法是改用更高效的算法。比如用动态规划,用类似于快速排序的分区,将递归改为循环的实现(如果有重复的子问题)

第二种方法,用空间换时间,例如可以用hash表,

2018/9/27 posted in  算法

OFFER 第3章 高质量的代码

3个方面确保代码的完整性
功能测试(基本功能)
边界测试(边界条件)
负面测试(错误输入)

16. 数的整数次方

实现函数Power(double base,int exponent),求base的exponent次方。不得使用库函数。

思路
用循环不断相乘是一种方法。但很容易想到,如果数值很大的时候,就会出现溢出。不能满足需求。

周密性考察
考虑到大数问题(我们这里不考虑大数问题)
考虑到指数的负数问题
考虑到指数为0的问题
关键的是大数问题,直接影响到我们的实现方式

优化思路(快速做乘方)
假如果我们的目标是求出一个数字的32次方。若果我们已经知道了它的16次方。那么只要在16次方的基础上在平方一次就可以了。而16次方是8次方的平方。这样就可以减少乘积的次数。

17. 打印从1到最大的n位数(如果用递归解决的话比较经典)

输入数字n,按顺序打印出从1到最大的n位十进制数,比如输入3,则打印出1,2,3,一直到最大的3为数999;

思路(这个思路很传统)
模拟加法

思路二(哈哈,这个思路很精妙)
递归打印。当为们想到全排列的时候,可以采用递归的方式

18. 删除链表的节点

在O(1)时间内删除链表节点。
给定单向链表的头指针和一个节点指针,定义在O(1)时间内删除该节点。

思考
要在O(1)时间内删除,普通做法是要找到所要删除节点的上一个节点,时间复杂度为O(n)。所以采取其他方法。

思路
可以将指定节点的后面节点的值赋值给当前节点。然后将后面节点的后面节点连接上当前节点。
有些特殊情况需要考虑。一是:当所要删除的是最后一个节点,则只能从最开始遍历。二是。所删除节点是倒数第二个节点。赋值后,直接将当前节点的next节点置位null;三是,指定的节点在原有链表中不存在的特殊处理。四是,链表本身是空

19. 正则表达式(未完成)

请实现一个函数用来匹配包含“."和""的正则表达式。模式中的字符”."表示任意一个字符,而“” 表示它前面的字符可以出现任意次(包含0次)。在本题中匹配是指字符串的所有字符匹配整个模式。例如字符串“aaa"与模式"a.a"和"abaca"匹配,当与”aa.a"和"ab*a"均不匹配。

思路
我的思路,遍历模式,一个以字符与需要匹配的字符相比较。遇到'.',需要匹配的字符跳过一个,模式也要跳过一个。因为'.'表示任意一个字符。如果字符与需要匹配的不相同,有两种选择,若果模式后面有*。则可以将其和*忽略,相当于有0个这个字符。然后与后面的进行比较.

20.表示数值的字符串(未完成)

请实现一个函数用来判定字符串是否表示数值。例如字符串"+100","5e2","-123","3.1412","-1E-16"都表示数值。

21.调整数组顺序使奇数位与偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分

思路
采用两个指针头尾,头尾同时判定,头为奇数++,尾为偶数++,头为偶数,直到尾为奇数然后交互, 这样在o(n)的时间复杂度

22.链表中倒数第K个节点

输入一个链表,输出该链表中倒数第k个节点。(最后一个节点为1)

思路
一般思路,遍历以便链表,几下链表长度。然后将从头开始遍历。走n-k步就可以了
也可以用一个辅助栈,将说有元素入栈,完事后将其弹出k个最后一个就是

优化思路
可以采用两个指针,前一个指针走了k步后,后一个指针开始走。这只需要遍历一次就能找到倒数第k个节点

23. 链表中环的入口节点

如果一个链表中包含环,如何找到环的入口节点?
Xnip2018-09-30_14-42-44

思路
链表的解决方案都有比较好的套路。
判定是否有环:搞两个指针一个走两步,一个走一步,有环的话,快指针会赶上慢指针。
找入口:如果是相遇,那么必然在环里,然后看环有多个元素。从相遇的那个节点开始,做遍历,必然会回到这,可以查看有多少个元素。加入为k个元素。还是用两个指针,p1,p2,p2向前走k个元素,然后p1与p2同时走。当相遇时,就是环的入口节点.

24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

思路
定义新的头节点,取出就节点的后一个节点,放到新的节点的前面(即采用头插法)。在最后将头旧的头节点的next置为null;反回新的头节点就可以了。这里要特别注意节点信息的丢失

从上面的思路可以看出也能用递归实现
递归的头条件还是那些条件。终止条件其实也是一样的。

25. 合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点任然是递增排序的。
Xnip2018-09-30_17-27-57

思路
比较两个链表的头,以头小的为准。边遍历,边比较与链表2的头。p.next与链表2比较。P.next大则将链表2的节点连接到链表1,更新链表2的头。如果表1的p.next==null,则直接将表2的放在表1的后面
时间复杂度为o(m+n);

优化思路(用递归实现的思路要清晰一些)(思想很重要)
思路其实和上面的一样。只是从不同的角度去看。这里从递归的角度去看问题。在比较两个链表的头部后,将小的放入新的链表的后面。这样重复炒作。就是一个递归问题。

26. 树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构.
Xnip2018-09-30_18-41-21

思路
按照树的处理,无非就时遍历。在这里要比较两树,可以遍历一个节点时,去比较是否是子树,遍历一个节点时,比较时否时子树。在遍历时节点时,本身就时一个递归的实现,在判断是否是子树时,又是一个递归遍历。

在这种多个流程里含有递归的情况很多。必过在全排列的时候,就时一个for循环是里有一个递归

2018/9/26 posted in  算法

OFFER

简明记录一些题目相关实现原理以及注意点。

基本数据结构

数组

字符串

链表

  • 树的遍历
    前序遍历
    中序遍历
    后序遍历
    主要掌握 这三种遍历的递归,与循环实现方式
    层序遍历

  • 树里的特例
    二叉搜索树

    红黑树

    栈与队列

递归与循环

查找和排序

  • 查找
    顺序查找
    二分查找
    哈希查找
    二叉排序树查找

  • 排序
    冒泡排序
    选择排序
    快速排序
    归并排序

回溯法

我理解的回溯法,更多的是利用“递归”的子问题特性将返回值带回上一层。上一层有多个这个子问题的一个综合。

动态规划

若果求一个的最优解,最大值或者最小值,而且该问题能分解成子问题。子问题能分解成更小的子问题。也就是大问题依赖于一系列的子问题的值(而不是单个问题的值,如果是单个问题的值,那么是回溯法)。在动态规划中,新的元素添加进去后,改变了原有的分配策略,也就是需要重新动态规划。

贪婪算法

2. 单例实现

注意多线程

3. 找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0~n-1的范围内。
找出数组中任意一个重复的数字。

一般思路
思路一:排序
排序O(nlogn)

思路二:搞一个hash表
O(n) 额外空间O(n)

思路三:根据题目给定的n的长度的数组里有0~n-1的数字这一特征。

3.1 不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。

不能修改原数组的话
一般解法是用一个额外数组或者hash表

优化思路
分析关键,n+1的数组里所有数字在1~n里。那么我可以通过将1~n里的数组分成两组,遍历数组看哪个小组里的数目大于其本身的组内元素的数目,则这个重复的数字就在哪个小组里。重复二分就可以找到了。
时间复杂度时O(nlogn),空间复杂度O(1)

分析思考方法
思路三的思考方法,是从特性触发。利用二分的思路。(哈哈,这就是为什么要对经典算法的深刻理解的原因了,很多问题就是对经典的问题的应用,理解了经典算法,就是见招拆招)

4. 二维数组中的查找

在一个二维数组中,每一行都按照从左到右递增的顺序排序。每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

Xnip2018-09-20_18-30-37

我的思路
哈哈,这个问题,是我自己实现的就是优化思路。

优化思路
选取最右角的数字,若果该数字等于要查找的数字,则查找结束。
若果该数字小于要查找的数字,则踢出这个数字所在行。若果该数字大于要查找的数字,则踢出这个数字所在列。这样每一步都可以缩小查找范围。

5. 替换空格

请实现一个函数,把字符串中的每个空格替换成%20,例如
"We are happy"输出"We%20are%20happy"

我的思路
开辟新的数组,边遍历边输出
时间复杂度O(N)*O(n),空间复杂度O(N)
如果是在原有数组的基础上,进行替换,则需要原有数组又足够的空间。
从上面的解法可以看出,多次空格时,将会有多次的后面元素的移动。
每多一个空格,就会将后面的元素多移动一遍。(这里就是优化的地方,当提出优化要求时,我们先分析有哪些需要优化的东西)

优化思路
还有一种思路,先找出有多少个空格,在遍历只做一次的移动。这时间复杂度时O(N)+O(N),时间复杂度比上面的低。

5.1合并两个有序数组,合并后有序

有两个排序数组A1与A2,内存在A1的末尾有足够的空间容纳A2,请实现以函数,吧A2中的所有数字插入A1中,并且是有序的。

简单思路
遍历A1,与A2的头比较,是的就加入到A1,并移动A1后面的东西。
这样就会有问题,没吃都会有大量的移动。O( n2 )

优化思路
还是从最后的元素进行比较,大的移动到最后,维持3个指针,
A1的尙未做过移动的的指针p1,A2的最后的指针p2,A1的从后向前移动后的指针。

6. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来打印出每个节点的值。

思考
一般的思路是将遍历到最后将链表反过来。然后在打印。
这样时间复杂度为O(N)*2,因为要遍历两边。

优化思路(嗯,就是这样的)
利用栈,或者递归,在打印的时候打印它的下一个节点。

7. 重建二叉树

输入某个二叉树的前序遍历与中序遍历结果,请重建二叉树
假如输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如
输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6} 重建二叉树

思路
(这个我的思路是有的,当真正实现还是花了点时间了的复习时,可以在用手在纸上写出来)
通过前序遍历确定根,根据中序遍历左右子树。然后重复这个过程,结束条件是到了叶子节点(无左右子树)

public static void main(String[] args){
        int [] pre = {1,2,4,7,3,5,6,8};
        int [] mid = {4,7,2,1,5,3,8,6};

        TreeNode head = createTree(pre,mid ,0,pre.length-1,0,mid.length-1);

        System.out.println("前序遍历");
        printPreTree(head);
        
        System.out.println("中序遍历");
        printMidTree(head);
        
        System.out.println("后序遍历");
        printAftTree(head);
    }

    public static TreeNode createTree(int[] pre,int [] mid,int preStart,int preEnd, int midStart,int midEnd) {
        
        if(preStart > preEnd || midStart > midEnd){//结束条件
            return null;
        }

        //前序遍历第一个节点是根节点
        TreeNode rootNode = new TreeNode(pre[preStart]);                    

        //找到在中序遍历的根节点的位置
        int location = rootLocation(mid,pre[preStart],midStart,midEnd);     

        TreeNode lefNode = null;
        //存在左子树
        if(preStart + 1 <= preStart + (midEnd - midStart)){
            lefNode = createTree(pre,mid,preStart+1,preStart + (location - midStart),midStart,location-1);
        }

        TreeNode rightNode   = null;
        //存在右子树
        if(location +1 <= midEnd){
             rightNode = createTree(pre,mid,preStart + (location - midStart) +1 , preEnd,location+1,midEnd);
        }
        rootNode.left = lefNode;
        rootNode.right = rightNode;
        return rootNode;
    }

    public static int rootLocation(int[] mid , int find,int midStart, int midEnd){
        //在midStart与midEnd之间寻找根节点的值
        int location = midStart;
        while(midStart <= midEnd){
            if(mid[midStart] == find){
                break;
            }
            midStart ++;
        }   
        return midStart;
    }

    //前序遍历
    public static void printPreTree(TreeNode root){
        if(root==null){
            return;
        }
        System.out.println(root.val);
        printPreTree(root.left);
        printPreTree(root.right);
    }

    public static void printMidTree(TreeNode root){
        if(root==null){
            return;
        }
    
        printMidTree(root.left);
        System.out.println(root.val);
        printMidTree(root.right);
    }

    public static void printAftTree(TreeNode root) {
        if(root == null){
            return;
        }
        printAftTree(root.left);
        printAftTree(root.right);
        System.out.println(root.val);
    }

8. 二叉树的下一个节点

给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左,右子节点的指针,还有一个指向父节点的指针。

9. 两个栈实现队列

用两个栈实现一个队列,队列的声明如下,请实现它的两个函数入队与出队。

思路(我没有思考的很全)
假设栈A,栈B,入队时,一次入队1,2,3,4,入队时,将入队的元素依次压入栈A中。出队时,将栈A的元素依次Pop到B中,并将最上面的元素取出(这个元素就是队首元素)。如果再由元素入队时,直接入栈A,再出队时,假如栈B还有元素直接出队。没有的化,就将栈A的元素依次出队放在栈B里。

9.1 两个队列实现一个栈

思路
对列A,队列B,依次入队1,2,3,4,入队1,入队列A,1,2,3,4,出栈时,将元素全部重新入队B。当对A只有最后一个元素时,就是出队的元素。在出队时,将队B的全部出队,留最后一个元素,就是需要出队的。入栈时直接放在非空的后面。

10. 斐波那契数列

Xnip2018-09-23_15-26-40

思路
从表达式可以看出用递归很好解决。

优化思路一
用递归表达式,因为f(n)有时候会被计算多次造成性能问题。所以只能改成循环来实现。临时记住已经计算过的值。性能更好。

优化思路二
还有一种用矩阵的解法logn,当要求矩阵运算。算法复杂,不太实用。

10.1 青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。

思考
这里主要是考察通过分析问题能力,数学建模的能力,解决问题的能力
第n级台阶可由第n-1级台阶,也可由第n-2级台阶而来。则实际上就是上面的斐波那契数列。

10.2 快速排序

//写出块排

10.3

11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,即数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。

思路
普通的遍历求最小值,当然是可行的,时间复杂度是O(n)。但失去了本题的意义。

优化思路
因为是部分有序的。可以通过二分查找思想,二分查找的时间复杂度时O(logn),通过二分查找定位最小值,二分查找由递归法,也有循环法

12. 矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左,右,上,下移动一格。若果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如下图求是否包含"bfce"的路径。
Xnip2018-09-25_21-25-35

13. 机器人的运动范围(我觉得这个题目有问题)

地上有一个m行n列的方格。一个机器人从坐标(0,0)的格子开始移动,它每次可以向左,右,上,下移动一格,但不能进入行坐标和列坐标的数位之和大于k的格子。

思考
感觉有两种思路,一种是直接用两个for循环
另一种是用向各个方向找,采用递归回溯的方式.
??我觉得这个题目是有问题的

14. 减绳子(这个问题很经典)

给你一根长度为n的绳子,请把绳子剪成m段。每段绳子长度k[0],k[1],k[m],请问k[0] *k[1]...可能的最大乘积是多少。

思考(采用动态规划的方法,将问题建模,也是比较难的)
长度为1时,不能分割,长度是2时,分割后找出最大值是确定的。长度是3时,分割后的最大值也是确定的。这样根据数学归纳法,每增加一个长度。我可以从与前面已经算好的最长的子问题一个个根据一定的规则推出当前的最大值。而这个规则,就是比较新分隔的两数字的大小中的最大值作为最大值。

优化思路
采用贪婪策略,当n>=5时,尽可能多的剪长度为3的绳子,当剩下的绳子长度为4时,把绳子剪成长度为2的绳子。(这个个实际上要找到一个贪心策略,说实话,需要比较高的数学能力)

15. 二进制中的1的个数

输入一个整数,输出数二进制表示中1的个数

思路
采取移位操作,将一个数每右移一位。然后与1做“与”运算。
1即00001,如果不为1,那么最后一位不是1,若果是1,那么最后一位就是1,然后继续右移。直到原数被移动到为0为止。(这样负数是有问题的,因为负数的表示时在首位是1,不断右移时,最终-1时, //-1, 100000000001,11111111111111,则构成了死循环,所以这种是有问题的)

为了避免循环,变相的思路
将1左移动,与原有数进行与运算,每次与运算,判定结果是否为0;为0就说明原有是1,不为0说明原有不为0;知道运行32位(假设是32位的机器)

优化思路(最好的解法)
把整数减去1,在和原整数做与运算,会把该整数最右边的1变成0。那么重复这么做。直到这个整数是0为止。
```
public static int numberFor1(int n ) {
int count = 0;
while(n!=0) {
n = (n - 1)&n;
count ++;
}

    return count;
}

### 16. 数值的整数次方


2018/9/18 posted in  算法

《算法图解》

最容易读懂的算法入门书

O()表示法

一些常见的大O表示
O(logn) 如二分查找
O(n) 如普通的线性查找
O(n * logn)
O( N2 )
O(n!)

算法的速度 指的并非时间,而是操纵数的增速
谈论算法速度时,我们说的是随着输入的增加,运行时间将一怎样的速度增加

随机访问
数组擅长随机访问。且在同一数组中元素类型都必须相同。(元素相同)

顺序访问
链表擅长顺序访问

数组与链表的存储
有时候将两者的结合起来存储。更快。利用了随机访问的快速,也可利用链表插入的快速

特别思考
在iOS中数组时可以存放不同对象类型的数据的。从这个可以推测,NSArray实际不是数组,而是由其他数据结构实现的数组的特性的数组。

排序

冒泡排序

比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

选择排序

选择排序实际上是每次将从原数据里取出最大,放到新数组的后面。时间复杂度时O( N2 )
特别说明:有个小疑问?因为选择排序每次的查找都会少一个元素,为什么时间复杂度还是N2呢。n,n-1,....2和1。即O(n* n*1/2)。大O表示法省略常数。即O( N2 )

特别说明
选择排序相比与冒泡排序只需要交换一次。

快速排序

分治法

分而治之是解决问题的的一种思路。
每一步都对问题的规模进行缩小。
分治工作原理(很重要)
1. 找到简单的基线条件
2. 确定如何缩小问题规模,使其最终符合基线条线条件
(也就是基线条件是确定的,要找到将问题规模缩小的方法)
特别说明:
分治法有点类似于数学归纳法。由小确定推到大确定。

示例
快速排序算法是一种分治法的典范。
所以找到规模的缩小方法。

  • 求sum(2,4,6)的和
    sum(2,4,6) = 2 + sum(4,6) ;
    大规模变成一个小规模,小规模在变成小规模,最终变成一个确定的值。然后又利用小规模的值逐级计算大规模。

  • 快速排序
    数组为空或者一个元素时本身就是有序的。
    规模缩小的方法是每一个有序都包括左有序+ 基准 + 右有序
    所以关键问题变成左右划分的问题了。

下面是划分的一种方法
quicksort

(这个个我没有实现上面对的东西?????)
快速排序是不稳定的算法。不稳定性在于分区交换时,如果有相同的值,那个值不一定是原有的那个值了。

归并排序

堆排序

递归

编写递归函数时,每个递归函数有两部分,退出条件递归表达式
在理解递归前需要理解调用栈

  • 5!(5的阶乘)
public static int factorial(int n){
        //5的阶乘
        if(n == 1){
            return 1;
        }
        return n * factorial(n-1);
     }
  • 斐波那契数列 1、2、3、5、8、13、21
// 1、2、3、5、8、13、21
     public static int factorial2(int n){
        if(n == 1){
            return 1;
        }
        if(n == 2){
            return 2;
        }
        return factorial2(n -1) + factorial2(n-2);
     }

散列表

散列函数

广度优先搜索

  1. 广度优先搜索
    用于解决无权有向图的最短路径问题

  2. 狄克斯特拉算法
    解决有带权有向图的最短路径问题

算法的宗旨:每次以最短路径为基准,计算其他未做过基准的节点的距离,重复。

示例
Xnip2018-09-17_11-56-35

有个疑问?狄克斯特拉算法这么保存路径
??????

  1. 贝尔曼-福德算法(不是重点) 解决带负边权的图的最短路径

贪婪算法

找局部最优解,最总得到的就是全局的最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。贪心算法不是对所有都能得到整体的最优解,选择贪心策略必须具备无后效性(某个状态以后的过程不会影响前面的状态)。

  • 教室调度
    希望在这间教室上尽可能多的课。
    具体做法
    选出结束最早的课(贪心策略),接下来选上节课结束后的能结束最早的课。
    实际的贪心策略就是找出结束最早的课

  • 背包问题

    包能装35磅的东西,有3000美元,30磅的音响。有2000美元,20磅的电脑,有1500美元15磅的吉他。

    采用贪心策略
    先选最贵的3000美元,30磅,但容不下其他的物件。
    实际的答案是可以是电脑与吉他一起放进去。
    所以背包问题用贪心解决不了,这个只能得到近似值
    因为不符合无后效性。当然背包问题可以用动态规划方法找到最优解。

  • 集合覆盖问题

    全美50州的观众收听得到广播。因为每个广播播出需要支付一定的费用。所以需要决定哪些广播播出。问题变成在50州里找出能满足要求的集合(实际上每一广播的覆盖就是一个集合)。50个元素的子集合有2的n次方个,n很大时,这个值将变的很大。所以用计算机计算也是不可能的。

    对于以上的问题只能得到近似算法
    贪心算法
    选出这样一个广播台,即它覆盖了最多的未覆盖州。重复第一步,直到覆盖了所有的州。(至于如何找到覆盖了最多的的未覆盖,就是集合运算,比如集合的交集,差集)

  • NP完全问题(nondeterministic polynomial time)

    旅行商问题
    我们假定不知道从那里离出发的5个城市。找出前往这5个城市的最短路径。
    5个城市,路线有2的5次方。加入城市在多点。就会有路线2的n次方条。这样就会是一个很大的数。

    集合覆盖问题
    上面已经讲过了

    以上两个问题都是NP完全问题。NP完全问题,一般都是采用贪婪算法求近似值。

    NP完全问题识别

    • 元素较少时算法的运行速度非常快。但随着元素的数量的增加,速度会变得非常慢。
    • 涉及到"所有组合"的问题通常都是NP问题
    • 不能讲问题分成小问题,必须考虑各种可能的情况
    • 如果问题涉及集合,集合覆盖,旅行商问题等且难以解决

动态规划

把多阶段过程转化成一系列单个问题,利用各个阶段之间的关系或者结果,逐个求解。

维基百科:
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

示例

IMG_0263

每一行都是利用上一行的计算的结果。(每一行都是上面物品的价值与重量的最优解),这里利用表来存计算结果数据。

小结
需要给定约束条件下优化某种之前时
问题可以分散为离散的子问题时
每种动态规划解决方案都涉及网络
单元格中的值通常就是你要优化的值
每个单元格都是一个子问题
动态规划解决方案的公式不同问题公式也不同,需要自己摸索

2018/9/17 posted in  算法