剑指offer思路总结

统一格式前注:

标题对应《剑指offer》题号
时间复杂度
空间复杂度
思路:包括解题思路和编程中的技巧
教训:编程过程当中须要注意的地方以及存在的惯性错误
html


1.赋值运算符函数(略)前端


2.实现Singleton模式(略)git


3.数组中重复的数字:
代码
时间复杂度:O(n);
空间复杂度:O(1);
思路:从0~n-1得到启发,各个数字值应当与所在数组下标一致,不一致则调换,当调换前的对比中出现相同值,则找到重复数字。程序员

衍生问题:长度为n+1的数组,数字在1~n范围内,要求不能改变原数组顺序
时间复杂度:O(nlogn)
空间复杂度:O(1)
思路:二分查找;具体就是按数组中间位置数字m,将数组分为1~m 和m+1~n;分别统计两部分中的数字在整个数组中出现的次数,若是次数大于这部分的数目,那么重复值就在这一部分中,不断重复上述过程,最终找到重复数字。
固然这种算法找不出全部的重复数字。web


4.二维数组中的查找:
代码
时间复杂度:
空间复杂度:
思路:从右上角数字切入:1.该数字等于目标数字,则找到;2.该数字大于目标数字,则删除该数字所在列;3.该数字小于目标数字,则删除该数字所在行;如此逐步缩小范围,直到找到目标数字;
教训:行数和列数的大于小于等于的设定必定要考虑清楚!!正则表达式


5.替换空格:
代码
时间复杂度:O(n)
空间复杂度:O(1)
思路:首先基于空格数规划新数组长度。然后用两个指针分别指向原数组末尾和新数组末尾,倒序进行插入替换。
教训:
【1】区分清三个长度:1.给定能用空间长度(length);2.原数组长度(str_length,要测出);3.新数组长度(str_new_length,经过str_length+empty_number*2可计算出)。
【2】而且注意while的条件(1.p1没到头;2.p1得在p2前面);
算法

衍生问题:两个数组的有序合并
思路:基本一致,就是从尾到头比较两个数组中的数字,并把较大的数字复制到第一个数组的合适位置。
编程


6.从尾到头打印链表:
代码
时间复杂度:
空间复杂度:
思路:两种思路:
【1】基于栈+循环的实现(从头至尾遍历入栈,再顺序出栈);
【2】基于递归的实现(就是每访问一个节点时,先递归输出它后面的节点,再输出该节点自身);
固然更推荐第一种思路由于递归层数较深时可能致使调用栈溢出。
教训:对于vector的语法要熟练掌握!!(insert)后端


7.重建二叉树:
代码
时间复杂度:
空间复杂度:
思路:分治思想!在二叉树的前序遍历中,第一个数字老是树的根节点的值,而在中序遍历中,根节点的值在序列的中间,由此咱们就肯定了两个序列的位置关系,经过递归思想能够不断构建下去。
教训:代码思路很简单:
【step1】输入合理性检测;
【step2】经过遍历中序序列,找出根节点所在;
【step3】基于找到的位置,将两个序列分为四个序列(注意index+1);
【step4】左右分别递归调用,实现不断的构建;
数组


8.二叉树的下一个节点:
代码
时间复杂度:
空间复杂度:
思路:
考虑到要求的是中序遍历时的下一个节点,所以能够将二叉树分3种状况分析:
【1】若是一个节点有右子树,则下一个节点就是其右子树的最左子节点;
【2】若是一个节点没有右子树并且这个节点是其父节点的左子节点,那么下一个节点就是其父节点;
【3】若是一个节点没有右子树并且这个节点是其父节点的右子节点,那么咱们能够沿着其父节点一路向上寻找,直到找到一个是它父节点的左子节点的节点;
教训:
具体编程时,三种状况里是要声明指针来负责指引的;
状况分析的顺序是1,3,2


9.用两个栈实现队列
代码
时间复杂度:
空间复杂度:
思路:栈1是数据进来后直接往里放的栈,所以push操做一行就行;栈2负责做为pop的中转,若是栈2没有数据,那么先要把栈1里的数据都出栈到栈2中,再取栈顶的数(top);若是栈2有数据,那么直接就取栈顶的数便可(top)。

衍生问题:用两个队列实现一个栈
思路:基本与原问题一致,也是一个队列负责往里放数据,pop时就是用另外一个队列来暂存以前的数。


附:递归的潜在问题:递归的本质是把一个问题分解成两个或者多个小问题
(1)重复的计算;(2)调用栈溢出;


10.斐波那契数列
时间复杂度:O(n)
空间复杂度:O(1)
思路:递归思路虽然直观,可是存在大量重复计算,全部不适用。采用自底向上的累加计算,0,1单独处理,然后两个数累加得下一个数,更新后如此循环,完成计算。

衍生问题:青蛙跳台阶问题、矩形覆盖问题
思路:本质上就是斐波那契数列(P78这种分步骤的思考方法值得借鉴!)


附:
1.常见的查找方法:
顺序查找、二分查找、哈希表查找、二叉排序树查找

2.常见的排序方法:
插入排序、冒泡排序、归并排序、快速排序等


11.旋转数组的最小数字
时间复杂度:O(logn)
空间复杂度:O(1)
思路:分三种状况讨论
【1】常规场景1,此时就是基于二分查找思想,若是mid位置的值大于p2的,那么最小数在后半部分,若是mid位置的值小于p1的,那么最小数在前半部分;
【2】特殊场景2: p1位置的值和p2的相等,此时最小就是p1位置的值(注意这一场景是做为while的判断条件的,若是出现,则while被整个跳过,函数会返回初始化为index_mid=p1的位置的值)
【3】特殊场景3:此时p1和mid和p2位置上的值大小相同,这样就只能进行总体遍历找到最小值;
教训:
总体思路是很清晰的,要注意的是特殊场景2的处理,在编程中是做为while的条件的
while(rotateArray[p1]>=rotateArray[p2])
为何这样写?是由于index_mid初始化为了p1,这样当出现场景2时,while就被跳过,将直接返回index_mid位置上的值,这是符合场景2的思想的;


附:回溯法
回溯法能够视为蛮力法的升级版,其很是适合由多个步骤组成的问题,而且每一个步骤有多个选项。
用回溯法解决的问题的全部选项能够形象的用树状结构表示。
过程:若是在叶结点的状态不知足约束条件,那么只好回溯到它的上一个节点再尝试其余选项。若是上一个节点的全部可能的选项都已经试过,而且不能达到知足约束条件的终结状态,则再次回溯到上一个节点。若是全部节点的全部选项都已经尝试过仍然不能达到知足约束条件的终结状态,则该问题无解。

一般回溯法算法适合用递归实现。


12.矩阵中的路径
代码
时间复杂度:
空间复杂度:
思路:
step1.输入合理性检测
step2.初始化一个标记矩阵,用于标记矩阵中元素是否已访问
step3.用两层循环来找到字符串起始字符在矩阵中的位置
step4.在递归函数里针对上下左右进行递归操做(注意其中的各类边界判断条件!)


13.机器人的运动范围
代码
时间复杂度:
空间复杂度:
思路:
和前面12题中矩阵中路径的思路一致,并且更为简单,只是多了个阈值检测而已。


附:应用动态规划求解问题的特色(4个):
【1】求解的是一个问题的最优解;
【2】总体问题的最优解是依赖各个子问题的最优解;
【3】咱们把大问题分解成若干个小问题,这些小问题之间还有相互重叠的更小的子问题;
【4】因为子问题在分解大问题的过程当中重复出现,为避免重复求解子问题,能够从下往上先计算小问题的最优解并存储下来,再以此为基础求取最大问题的最优解(“从上往下分析问题,从下往上求解问题”)


14.剪绳子
时间复杂度:
空间复杂度:
思路:能够采用动态规划或者贪婪算法来解决。
动态规划:从下往上累进式计算;
贪婪算法:当n>=5时,咱们尽量多地剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。


15.二进制中1的个数
代码
时间复杂度:
空间复杂度:
思路:
常规解法:就是保持目标数不动,1不断进位,实现逐位的1次数统计;中规中矩,循环次数等于整数二进制的位数;
巧妙解法:把一个整数减去1,再和原整数作&运算,会把这一整数的最右边的1变为0.那么一个整数的二进制表示中有多少个1,就能够进行多少次这样的操做。相较于常规解法,该方法具备更少的循环次数。

衍生问题:
(1)用一条语句判断一个整数是否是2的整数次方
思路:若是这个整数是2的整数次方,那么它的二进制表示中只有一位是1,所以能够采用前面的思路,将这个数减1再和本身作&运算,这样结果应该是0;
(2)输入两个整数m和n,计算须要改变m的二进制表示中的多少位才能获得n
思路:step1.求两个数的异或;step2.统计异或结果中1的位数(一样采用前面的思路);


16.数值的整数次方
代码
时间复杂度:
空间复杂度:
思路:次方很好算,主要是特殊状况要考虑全面。特殊状况包括:【1】base为0且exponent<0,这种状况只能为0;【2】当base不为0,可是exponent时,须要先取绝对值,计算完再取倒数。
次方的计算也有优化之处,如P112的公式所分析的,经过迭代,n次迭代就不用计算n次了。

咱们要注意:因为计算机表示小数(包括float和double型小数)都有偏差,咱们不能直接用等号(==)判断两个小数是否相等。若是两个小数的差的绝对值很小,好比小于0.0000001,就能够认为它们相等。
在这里插入图片描述


17.打印从1到最大的n位数

时间复杂度:
空间复杂度:
思路:采用基于递归的全排列方法,数字的每一位均可能是0~9中的一个数,而后设置下一位。递归的结束条件是咱们已经设置了数字的最后一位。


18.删除链表的节点
题1:在O(1)时间内删除链表节点
时间复杂度:O(1)
空间复杂度:O(1)
思路:整体思路就是把下一个节点的内容复制到须要删除的节点上覆盖原有的内容,再把下一个节点删除,这样就至关于把当前须要删除的节点删除了。此外还须要考虑待删除节点为:1.尾节点;2.链表只有一个节点的状况。

题2:删除链表中重复的节点(很考验编程能力!!)
代码
时间复杂度:
空间复杂度:
思路:pPreNode指向前一节点,pNode指向当前节点,pNext指向下一个节点。(pTodelete指向pNode)

//核心维护pre和cur两个指针,本质上仍是快慢指针思想的体现
ListNode *deleteDuplicates(ListNode *head) {
    if (! head || ! head->next) 
    	return head;
    
    ListNode *start = new ListNode(0);
    start->next = head;
    ListNode *pre = start;
    while (pre->next) 
    {
        ListNode *cur = pre->next;
        while (cur->next && cur->next->val == cur->val) 
        	cur = cur->next;
        if (cur != pre->next) 
        	pre->next = cur->next;
        else 
        	pre = pre->next;
    }
    return start->next;
}

19.正则表达式匹配

时间复杂度:
空间复杂度:
思路:

解这题须要把题意仔细研究清楚,反正我试了好屡次才明白的。
首先,考虑特殊状况:
     1>两个字符串都为空,返回true
     2>当第一个字符串不空,而第二个字符串空了,返回false(由于这样,就没法
        匹配成功了,而若是第一个字符串空了,第二个字符串非空,仍是可能匹配成
        功的,好比第二个字符串是“a*a*a*a*”,因为‘*’以前的元素能够出现0次,
        因此有可能匹配成功)
以后就开始匹配第一个字符,这里有两种可能:匹配成功或匹配失败。但考虑到pattern
下一个字符多是‘*’, 这里咱们分两种状况讨论:pattern下一个字符为‘*’或
不为‘*’:
      1>pattern下一个字符不为‘*’:这种状况比较简单,直接匹配当前字符。若是
        匹配成功,继续匹配下一个;若是匹配失败,直接返回false。注意这里的
        “匹配成功”,除了两个字符相同的状况外,还有一种状况,就是pattern的
        当前字符为‘.’,同时str的当前字符不为‘\0’。
      2>pattern下一个字符为‘*’时,稍微复杂一些,由于‘*’能够表明0个或多个。
        这里把这些状况都考虑到:
           a>当‘*’匹配0个字符时,str当前字符不变,pattern当前字符后移两位,
            跳过这个‘*’符号;
           b>当‘*’匹配1个或多个时,str当前字符移向下一个,pattern当前字符
            不变。(这里匹配1个或多个能够当作一种状况,由于:当匹配一个时,
            因为str移到了下一个字符,而pattern字符不变,就回到了上边的状况a;
            当匹配多于一个字符时,至关于从str的下一个字符继续开始匹配)
以后再写代码就很简单了。

20.表示数值的字符串
代码
时间复杂度:
空间复杂度:
思路:表示数值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A为数值的整数部分,B紧跟着小数点为数值的小数部分,C紧跟着e或者E为数值的指数部分。
所以实现的时候分为三个部分:首先是对整数部分进行分析,接着是对小数部分进行分析,而后是对指数部分进行分析


21.调整数组顺序使奇数位于偶数前面
代码
时间复杂度:
空间复杂度:
思路:新建一个vector,先遍历一遍,遇到奇数就push_back;再遍历一遍,遇到偶数就push_back


22.链表中倒数第k个节点
代码
时间复杂度:
空间复杂度:
思路:两个指针都先指向头指针,然后第一个先走k步,然后两个指针同时后移直到末尾,此时第二个指针所指就是倒数第k个节点。
教训:
step1.输入合理性检测(包括链表为空、k为非正数)
step2.一个次数为k的循环,作好准备的同时,也处理了当链表长度小于k的状况
step3.就是两个指针同步后移直到末尾

衍生问题:求链表的中间节点
思路:两个指针,一个一次走一步,一个一次走两步,这样当走的快的指针走到链表的末尾时,走的慢的指针正好在链表的中间。


23.链表中环的入口节点
代码
时间复杂度:
空间复杂度:
思路:
step1.利用快慢两个指针,可以在有环的前提下得到一个环内的相遇节点;
step2.基于这个环内相遇节点,统计环内的节点数目;
step3.两个指针策略,都先指向头指针,一个先移动环内节点数目的步数;
step4.然后两个指针再同步移动,此时相交的节点位置就是环口位置。


24.反转链表
时间复杂度:
空间复杂度:
思路:定义三个指针,分别指向当前遍历到的节点、它的前一个节点和后一个节点;
教训:在while循环中,要声明一个pNext来指向pNode的下一个节点,须要注意的是,每次改变的都是pNodeBefore和pNode之间的链接,返回的最终是pNodeAfter指针!(见P143图)


25.合并两个排序的链表
代码
时间复杂度:
空间复杂度:
思路:核心思想是递归!具体来讲就是两个链表中头结点比较,小的那个放入前面,而后调整小的这个链表传入递归函数的头指针,递归继续进行比较。(此外为保证算法鲁棒性,还要针对两个链表是否为空进行检测)
教训:
step1.首先对两个链表进行空链表检测
step2.定义一个最终结果的头指针resultHead
step3.按照两个链表头指针所指值的大小关系,给resultHead赋值,然后resultHead->next=Merge
继续进行后续的递归操做。


26.树的子结构
添加连接描述
时间复杂度:
空间复杂度:
思路:分两个步骤
step1.在树A中找到和树B的根节点的值同样的节点R;
step2.判断树A中以R为根节点的子树是否是包含和树B同样的结构
教训:采用递归的方法,注意对nullptr的判断!


27.二叉树的镜像
代码
时间复杂度:
空间复杂度:
思路:(核心固然采用的是递归方法)很直观,采用的是前序遍历,若是遍历到的节点有子节点,就交换它的两个子节点。当交换完全部非叶子节点的左右子节点以后,就获得了树的镜像。


28.对称的二叉树
代码
时间复杂度:
空间复杂度:
思路:构造一种前序遍历算法的对称遍历算法,这样两个算法都遍历一次,若是序列相同,则认为此二叉树为对称二叉树。
教训:具体实现时,沿用的递归的方法,比较就至关于一颗树的左子节点和另外一颗树的右子节点间的比较。


29.顺时针打印矩阵
在这里插入图片描述
代码
时间复杂度:
空间复杂度:
思路:以上面的图为核心,打印分为四个阶段
主要注意在后两个阶段加入的top和bottom,left和right的比较,这里的理解是,前两阶段操做,对于不一样的矩阵状况,通常都有,然后两种则不必定有,全部要加入条件判断。

书上的理解是:第一步老是须要的,第二步的前提条件是终止行号大于起始行号;第三步须要的前提条件是圈内至少有两行两列,这就解释了为何要有top和bottom的对比;同理,第四步须要的前提条件是至少有三行两列,这就解释了为何要有left和right的对比。


30.包含min函数栈
时间复杂度:
空间复杂度:
思路:咱们须要一个数据栈和一个辅助栈,数据栈就是常规的数据存储,辅助栈每次把当前最小值入栈(简单来讲,当来一个数时,若是这个数入栈后是栈中最小的数,那么辅助栈一样存放的是这个数,而若是这个数并非栈中最小的数,那么辅助栈会把栈顶的数再存一次)
教训:注意value和my_min_value辅助栈的栈顶数字比较前,先判断该栈是否为空(这是很严谨的,由于值比较前,是必需要有值的,否则连比较都无法实现)


31.栈的压入、弹出序列
代码
时间复杂度:
空间复杂度:
思路:
1.若是下一个弹出的数字恰好是栈顶的数字,那么直接弹出;
2.若是下一个弹出的数字不在栈顶,则把压栈序列中尚未入栈的数字压入辅助栈,直到把下一个须要弹出的数字压入栈顶位置;
3.若是全部的数字都压入栈后,仍然没有找到下一个弹出的数字,那么该序列不多是一个弹出序列;
教训:
具体编程时,依照思路的步骤,重点关注条件判断的处理!


32.从上到下打印二叉树
代码
时间复杂度:
空间复杂度:
思路:就是对树的常规逐层遍历,采用deque的数据结构,能够后端插入,前端弹出

衍生问题1:分行从上到下打印二叉树
代码
思路:核心仍然是利用队列queue这一数据结构,为了把每一行单独打印到一行里,须要两个变量:一个变量表示在当前层中尚未打印的节点数this_line,另外一个变量表示下一层节点的数目next_line。
教训:开头节点的处理在while循环里要处理好,重点关注!!

衍生问题2:之字形打印二叉树
代码
思路:按之字形顺序打印二叉树须要两个栈。咱们在打印某一层的节点时,把下一层的子节点保存到相应的栈里:
【1】若是当前打印的是奇数层,则先保存左子节点再保存右子节点到第一个栈里;
【2】若是当前打印的是偶数层,则先保存右子节点再保存左子节点到第二个栈里


33.二叉搜索树的后续遍历序列
代码
时间复杂度:
空间复杂度:
思路:
首先明确,二叉搜索树是有大小性质的:左子树节点<根节点<右子树节点
采用的是递归的思路,首先根节点位于序列的尾部,然后咱们能够根据根节点,到序列中经过大小比较,将序列划分为左子树节点序列和右子树节点序列,然后就能够分左右子树递归进行判断。
教训:
务必注意length-1这一循环边界!同时要注意划分位置mid_index的取值!!!


34.二叉树中和为某一值的路径
代码
时间复杂度:
空间复杂度:
思路:由于路径老是从根节点出发的,因此说对二叉树的遍历须要首先访问根节点,3种遍历方法中只有前序遍历知足。
此外这种访问方式须要典型的栈数据结构的支持,可是在实际编程中,咱们采用的是vector,这是从结果存放角度考虑的,使用的是vector的push_back方法和pop_back方法。
教训:
这里采用的递归思路颇有意思,递归函数是进来就把节点存入队列,而后再判断是否和等于预约值以及是否到叶结点,这样作可行是由于该递归函数是没有返回值的,能够说存放到result的惟一途径就是这一个组合判断。(注意传入函数的是result和each_line的引用!!)


35.复杂链表的复制
代码
时间复杂度:O(n)
空间复杂度:
思路:思路上是很直观的,分为3个步骤:
【step1】把链表中每一个复制的节点链接到原节点后面;
【step2】把原链表各节点的random链接,复制到复制节点的random;
【step3】调整链接,把奇数位置的节点链接起来就是原链表,偶数位置的节点链接起来就是复制出来的链表;
教训:
在编程过程当中,注意第三步时,pNode要调整到pnext后面,这是由于在while条件中,只能用pNode !=nullptr,这是由于pnext可能一开始就是空的,此时是不能用pnext->next !=nullptr来做为判断条件的,要特别注意!!!


36.二叉搜索树与双向链表
代码
时间复杂度:
空间复杂度:
思路:核心思路是“中序遍历+递归”
中序遍历的特色是按照从小到大的顺序遍历二叉树的每个节点。当咱们遍历到根节点时,它的左子树已经转换为一个排序的链表了,而且处于链表中的最后一个节点是当前值最大的节点,此时须要的就是把根节点和这个最大值节点链接起来,接着再去遍历右子树,把根节点和右子树中最小的节点链接起来。经过递归执行这一过程,实现双向链表的构建。
教训:
首先,在遍历完整个二叉树后,为了返回双向链表的头指针,是须要while遍历一下的;
此外,在递归函数中,注意根节点和左边最大节点的链接操做,以及pLastNodeInList指针的更新;
pLastNodeInList是一个指向已转好链表中最后元素的指针,所以在传入时是按照指针传入的(
)**


37.序列化二叉树(其实就是遍历和重建二叉树)
时间复杂度:
空间复杂度:
思路:


38.字符串的排列
代码
时间复杂度:
空间复杂度:
思路:实际上思路是很直观的,每次迭代都只考虑一位数字。
具体的,第一步求全部可能出如今第一个位置的字符,即把第一个字符和后面全部的字符交换;
第二步固定第一个字符,求后面全部字符的排列。这时候咱们仍然把后面的全部字符分红两个部分:后面字符的第一个字符,以及这个字符后面的全部字符,而后把第一个字符逐一和后面的字符交换。
教训:
之因此编程的和书上有区别,是由于书上传入的是str的指针,所以在迭代过程当中的值交换,在出迭代时要还原回来,而实际编程中采用的是值传递,所以不须要还原这一步骤。

衍生问题1:若是不是求字符的全部排列,而是求字符的全部组合
思路:咱们能够吧求n个字符组成长度为m的问题分解成两个子问题,分别求n-1个字符中长度为m-1的组合,以及求n-1个字符中长度为m的组合。这两个子问题均可以用递归的方式解决。

衍生问题2:正方体三组相对的面上的4个顶点的和都相等
思路:这至关于获得8个数字的全部排列,而后判断有没有某一个排列符合题目给定的条件,即三组相等。

衍生问题3:8皇后问题
思路:咱们用一个长度为8的数组array,数组中第i个数字表示位于第i行的皇后的列号。数组先用0~7进行初始化,然后进行全排列。(由于咱们用不一样的数字初始化数字,全部任意两个皇后确定不一样列,所以只须要判断每一种排列是否在同一条对角线上,即对于数组的两个下标i和j,是否有i-j=array[i]-array[j]或者j-i=array[j]-array[i])



经验Tips:
1.对于C++程序员来讲,要养成采用引用(指针)传递复杂类型参数的习惯,这对提升代码的时间效率有好处;
2.递归尽管写法上很简洁,可是考虑到若是小问题中有互相重叠的部分,则时间效率上不好;对于这种题目,咱们能够用递归的思路来分析问题,可是写代码时能够用数组来保存中间结果,再基于循环来实现。绝大部分动态规划算法的分析和代码实现都是分这两个步骤实现的。
3.代码的时间效率是能够反映一我的的基本功的,好比一样是查找算法,有O(n) 的时间复杂度,也有O(logn)和O(1)。


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

思路2代码
时间复杂度:都是O(n)
空间复杂度:
思路:
1.基于快排的思路:在数组中随机选择一个数字,而后调整数组中数字的顺序,使得比选中的数字小的数字位于选中数字左边,比选中数字大的数字位于选中数字右边,(1)若是该选中数字下标恰好是n/2,则这个数字就是中位数(2)若是该数字下标大于n/2,则中位数位于其左边,能够在左边重复上述查找查找;(3)若是该数字下标小于n/2,则中位数位于其右边,能够在右边重复上述查找查找;(此外固然还要考虑非法输入的问题)
2.基于统计的思路:咱们遍历给定数组,遍历过程当中保持两个数,一个是数组中的数字,一个是次数;当咱们遍历到下一个数字时,若是下一个数字和咱们以前保存的数字相同,则次数加1,不然次数减1.若是次数为零,则咱们须要保存下一个数字,并把次数设置为1。
因为咱们要找的数字出现的次数比其余全部数字出现的次数之和还要多,那么要找的数字确定是最后一次把次数设置为1时对应的数字。
两种思路时间复杂度相同,区别在于前者会改变原数组中数字的顺序,然后者则不改变原数组中数字的顺序。


40.最小的k个数(能够考察大数据问题)
代码
时间复杂度:
空间复杂度:
思路:
1.基于快排的思路,利用上面的Partition函数,只是这里和第k位比较,而不是和middle比较;
2.基于最大堆这一数据结构,咱们每次在O(1)时间内获得已有的k个数字中的最大值,可是须要O(logk)时间完成删除及插入操做;
附:对堆的操做介绍


41.数据流中的中位数
代码
时间复杂度:
空间复杂度:
思路:采用了基于vector实现的最大堆和最小堆(最大堆的第一个数是最小值,最小堆的第一个数字是最大值)。
思路就是左边为最大堆,右边为最小堆,左边保持恒小于右边。
当已有数目为偶数时,新来的数要放入最小堆;当已有数目为奇数时,新来的数要放入最大堆。
若是待放入最大堆的数小于max[0],则要进行堆的调整(压入和弹出),不然应放入最小堆中;反之同理。

涉及的语法:对堆操做的介绍
push_heap(max.begin(),max.end(),less &lt; i n t &gt; &lt;int&gt; ());
pop_heap(max.begin(),max.end(),less &lt; i n t &gt; &lt;int&gt; ());
push_heap(min.begin(),min.end(),greater &lt; i n t &gt; &lt;int&gt; ());
pop_heap(min.begin(),min.end(),greater &lt; i n t &gt; &lt;int&gt; ());


42.连续子数组的最大和
代码
时间复杂度:O(n)
空间复杂度:O(1)
思路:从头开始累加,若是当累加下一个数时,前面的和<=0,此时再累加下一个数确定是小于这个数的,所以此时应当将和置零,从当前值开始从新进行累加;在计算过程当中,保持最大的和记录,最后返回的即为最大值。
本质上是动态规划法的循环实现:
在这里插入图片描述


43.1~n整数中1出现的次数
代码
时间复杂度:O(ln(N)/ln(10)+1)
空间复杂度:
思路:逐个判断每一位的状况。
【1】若是这一位是0,则这一位出现1的次数由更高位决定;
【2】若是这一位为1,则这一位出现1的次数不只受更高位影响,并且还受低位影响;
【3】若是这一位数字大于1,则这一位出现1的次数仅由更高位决定;

int NumberOf1Between1AndN_Solution(int n)
    {
        if(n<0)
            return 0;
         
        int icount=0;
        int ifactor=1;
         
        int ilowernum=0;     //低位数字
        int icurrentnum=0;   //当前位数字
        int ihighernum=0;       //高位数字
         
        while(n / ifactor !=0)
        {
            ilowernum= n-(n/ifactor)*ifactor;              //这3组公式很关键!!
            icurrentnum=(n/ifactor)%10;
            ihighernum = n/(ifactor*10);
             
            switch(icurrentnum)
            {
                case 0:
                    icount += ihighernum*ifactor;
                    break;
                case 1:
                    icount +=ihighernum*ifactor+ilowernum+1;
                    break;
                default:
                    icount +=(ihighernum+1)*ifactor;
                    break;
            }
            ifactor *=10;
        }
        return icount;
    }

44.数字序列中某一位的数字
时间复杂度:
空间复杂度:
思路:这一问题的核心就是找规律,10,90,900,…找到规律就容易处理了。
step1.首先是一个计算该位数有多少数字的函数,核心公式就是10^(digitic-1)*9;
step2.若是说第n位比这个大,那么n减去该位所拥有的数字,而后再循环比较;
step3.若是说第n位小于这个位全部的数的数目了,那么就说明n是在该位数之中,然后能够用一个函数单独找出这个数字;


45.把数组排成最小的数
代码
时间复杂度:
空间复杂度:
思路:为简单起见,同时为了不隐形大数问题,咱们定义compare函数,将两个数转化为字符串来进行比较。
具体思路就是先将各个字符串排序,然后拼接起来便可(这里用到了内置的sort函数,固然compare方法要本身写)

string PrintMinNumber(vector<int> numbers) {
         
        string str="";  //用于返回目标字符串
        //输入检查
        if(numbers.size()==0)
            return str;
         
        int n=numbers.size(); //获取目标数组的长度,即有多少个数要进行处理
         
        sort(numbers.begin(),numbers.end(),compare);  //对给定数组进行排序
         
        for(int i=0;i<n;i++)   //对排好序的数组进行拼接
        {
            str += to_string(numbers[i]);
        }
         
        return str;
    }
         
    static bool compare(int a,int b)
    {
        string str1= to_string(a);
        string str2= to_string(b);
             
        return (str1+str2)<(str2+str1);           
    }

46.把数组翻译成字符串
时间复杂度:
空间复杂度:
思路:
思路1:递归思想,咱们定义函数f(i)表示从第i位数字开始的不一样翻译的数目,那么f(i)=f(i+1)+g(i,i+1)*f(i+2)。当第i位和第i+1位两位数字在10~25的范围内时,函数g(i,i+1)的值为1,不然为0。
潜在问题:重复计算比较严重

思路2:自下而上解决问题,这样就能够消除重复的子问题。
具体实现方面,采用一个和字符串等长的数组,从最后一位开始,最后一位初始化为1,然后向前计算,每位等于上一位的值,或者是上一位和上上一位之和。最后返回的是该数组的第一位数。

//Leetcode NO.91:思路就是动态规划,只是其中加入了一些判断条件
class Solution {
public:
    int numDecodings(string s) {
        if(s.size()==0 || (s.size()> 0 && s[0]=='0'))
            return 0;
        
        vector<int> result(s.size()+1, 0);
        result[0]=1;
        
        for(int i=1;i<s.size()+1;i++)
        {
            result[i]=(s[i-1]=='0') ? 0 : result[i-1];
            if(i>1 && (s[i-2]=='1' || (s[i-2]=='2' && s[i-1]<='6')))
               result[i] +=result[i-2];
        }
        return result.back();
    }
};

47.礼物的最大价值
时间复杂度:
空间复杂度:
思路:采用动态规划思想。(本质上求解思路是从右下到左上的,即与题目相反)
用一个1*n的数组存储和,表明的是到达该位置时所能得到的最大价值。(这个一维数组是从二维矩阵简化来的,由于在逐层计算时,到达坐标(i,j)的格子时可以拿到的礼物的最大价值只依赖于(i-1,j)和(i,j-1)这两个格子,所以第i-2行以及更上面的全部格子礼物的最大价值实际上没有必要保存下来,所以采用了一个一维数组代替)

这个一维数组存放的是上一行n-j个值和本行j个值,并且是在不断更新的。(就是说这个计算过程是反着的)


48.最长不含重复字符的子字符串
时间复杂度:
空间复杂度:
思路:采用动态规划算法
首先定义函数f(i)表示以第i个字符为结尾的不包含重复字符的子字符串的总长长度。
场景1.若是第i个字符以前没有出现过,那么f(i)=f(i-1)+1;
场景2.若是第i个字符以前已经出现过,则咱们先计算第i个字符和它上次出如今字符串中的位置的距离,并记为d;
场景2.1 第一种状况是d小于或者等于f(i-1),此时第i个字符上次出如今f(i-1)对应的最长子字符串之中,所以f(i)=d;
场景2.2 第二种状况是d大于f(i-1),此时第i个字符上次出如今f(i-1)对应的最长字符串以前,所以仍然有f(i)=f(i-1)+1;


49.丑数
代码
时间复杂度:
空间复杂度:
思路:空间换时间的思路。
用一个数组来存放从小到大顺序形式的丑数,用三个指针标记三个位置,这三个位置表明三种丑数新加入数的位置,由于在这三个指针前的数再乘该丑数,结果已经存在于数组中,所以没有重复计算的必要。


50.第一个只出现一次的字符
问题1:字符串中第一个只出现一次的字符(只有大小写字母)
代码
时间复杂度:O(n)
空间复杂度:O(1)
思路:利用哈希表。
第一轮遍历设定两个数组,分别统计大小和小写字母出现的次数,与此同时用两个数组记录相应的下标;
这样在第二轮遍历时,就不须要对str进行遍历了,而只须要遍历大小为26的哈希表,注意ASCII表中(a=97>A=65)。

问题2:输入两个字符串,从第一个字符串中删除第二个字符串中出现过的全部字符。
思路:利用长度为26的哈希表便可;

问题3:删除字符串中全部重复出现的字符。
思路:一样是利用哈希表;

问题4:英语中的变位词检查。
思路:一样是利用哈希表,初始化为0,第一个单词遍历时,出现的字母对应位置+1;第二个单词遍历时,出现的字母对应位置-1,最后统计全部位置的和为0,则说明两个单词互为变位词。

问题5:字符流中第一个只出现一次的字符
思路:一样是利用哈希表,初始化值为-1,首次出现时把值赋为该字符的位置,再次出现使,把它赋为-2,这样当咱们要寻找到目前为止从字符流中读出的全部字符中第一个不重复的字符时,只须要扫描整个数组,并从中找出最小的大于等于0的值对应的字符便可。


51.数组中的逆序对

时间复杂度:
空间复杂度:
思路:


52.两个链表的第一个公共节点
代码
时间复杂度:O(m+n)
空间复杂度:
思路:
step1.首先基于子函数,分别统计两个链表的长度;
step2.对较长的链表,先走差值步;
step3.然后两个链表同步比较和后移,直到找到第一个相等的数;


53.在排序数组中查找数字
代码
问题1:数字在排序数组中出现的次数
时间复杂度:O(logn)
空间复杂度:
思路:利用二分查找算法,整体思路是先用二分查找找出目标数第一次出现所在位置start,然后再用二分查找找出目标数最后一次出现所在位置end,然后次数就等于numebrs=end-start+1。
教训:若是数组中不包含k,则返回-1做为标志,这个在主函数中做为numbers计算前的判断条件。

这里还遇到一个图灵完备性的问题,简单来讲就是若是大循环是if(data.size() !=0),那么return应该保证if成立与否都应该有值,也就是说这种写法时,return在内部是不够的,在if外还应该有return。(这个东西从语法上来讲是没问题的,只是由于完备性考虑,因此当考虑不彻底时,就会出现编译错误)

问题2: 0~n-1中缺失的数字
思路:本质上仍是利用二分查找,问题转换为在排序数组中找出第一个值和下标不相等的元素。

问题3:数组中数值和下标相等的元素
思路:一样是利用二分查找,若是第i个数字的值大于i,那么它右边的数字都大于对应的下标,咱们均可以忽略。下一轮查找咱们只须要从它的左边的数字中查找便可。


54.二叉搜索树的第k大节点
代码
时间复杂度:
空间复杂度:
思路:考虑到二叉搜索树是大小有序的二叉树,所以本质上考察的是中序遍历算法。(递归实现,k的指针传递要注意)
具体实现方面:先一路检测左子树直到叶子结点,然后判断这个k是否符合,不符合则减1,由于是从最小值开始的,然后再检查右子树。
教训:“先左–再判断k–再右”


55.二叉树的深度
代码
时间复杂度:
空间复杂度:
思路:一种层层分割的思想,若是一棵树只有一个节点,则深度为1。若是根节点只有左子树而没有右子树,则树的深度为左子树深度+1;若是根节点只有右子树而没有左子树,则树的深度为右子树深度+1;若是根节点既有左子树又有右子树,则树的深度就是二者中深度较大的+1。

int TreeDepth(TreeNode* pRoot)
    {
        //输入检测
        if(pRoot==nullptr)
            return 0;
         
        int nleft=TreeDepth(pRoot->left);
        int nright=TreeDepth(pRoot->right);
         
        return (nleft>nright)?(nleft+1):(nright+1); //思路就是层层分割,递归进行,返回的就是左右子树中相对深度较大的
    }

衍生问题:平衡二叉树的判断
基于后序遍历的版本代码
思路:这种借助了后序遍历的思想。在遍历一个节点以前就已经遍历了它的左右子树。只要在遍历每一个节点的时候记录它的深度,就能够一边遍历,一边判断每一个节点是否是平衡的。
借助上一问的递归版本代码
思路:这种比较好理解,就是不断递归计算深度,而后每次递归比较一下左右两边深度。


56.数组中数字出现的次数
代码
时间复杂度:
空间复杂度:
思路:利用的是异或操做的抵消效果。
step1.首先对数组进行一轮异或遍历,因为数组中有两个只出现一次的数,所以结果中必然是有位是1的;
step2.对于step1中遍历的结果,找到结果中是1的第一个位置(这里是位运算),该位置将用于对数组进行划分;
step3.而后再次对数组进行遍历,根据上面位为1的特色,数组将被划分为两个数组,每一个数组里只有一个出现次数为1的数字,这样在这个数组上进行异或遍历后,结果就是那个只出现一次的数。


57.和为s的数字
代码
时间复杂度:O(n)
空间复杂度:O(1)
思路:(被耍了一道。。)从两头开始,若是和大于sum,则 j–,若是和小于sum,则 i++。若是等于sum,则直接返回。问题里要什么乘积最小的,实际上就是第一组。。

衍生问题:和为s的连续正数序列
代码
时间复杂度:
空间复杂度:
思路:思路与前面两个数和为s基本一致,也是两个下标移动的策略。
教训:
这里不是从两头开始, 而是从1,2开始,并且while的条件也变为 i<(1+sum)/2,要注意的就是对每一轮累加的优化,以及找到后的存放。


58.左旋转字符串
代码
时间复杂度:
空间复杂度:
思路:归纳为“拆-本翻-整翻”
step1.根据给定的翻转个数,将字符串分为先后两个部分;
step2.先分别翻转先后部分;
step3.再翻转整个字符串,这样就获得告终果。
教训:
注意输入合理性检测,要考虑全面,返回的是str。

if(str.size()==0 || n<0 || n>str.size())
            return str;

59.队列的最大值
代码
时间复杂度:
空间复杂度:
思路:用一个两端开口的队列(deque)存放潜在最大值的下标
step1.首先初始化第一个划窗,找出最大的放入deque以及result队列;
step2.接着遍历,若是已有数字小于待存入的数字,那么已有数字从队列头部弹出;若是队列头部的数字已经不在划窗中(靠下标之差肯定),则该头部数字从队列头部弹出;
教训:必须想清楚基本原理!vector和deque不能弄混了。


60.n个骰子的点数
暂无代码
时间复杂度:
空间复杂度:
思路:两个6n+1长度的数组,交替进行更新


61.扑克牌中的顺子
代码
时间复杂度:
空间复杂度:
思路:三步骤,将5张牌视为一个长度为5的数组;
step1.首先把数组排序;
step2.其次统计数组中0的个数;
step3.最后统计排序以后的数组中相邻数字之间的空缺总数。若是空缺的总数小于或者等于0的个数,那么这个数组就是连续的,反之则是不连续;


62.圆圈中最后剩下的数字
思路1代码
时间复杂度:O(mn)
空间复杂度:O(n)
思路:采用循环链表的数据结构,循序渐进的进行循环删减

思路2代码
时间复杂度:O(n)
空间复杂度:O(1)
思路:基于找规律,获得以下公式
在这里插入图片描述

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if(n < 1 || m < 1){
            return -1;
        }
        int last = 0;
        for(int i = 2; i <= n; i++){
            last = (last + m) % i;
        }
        return last;
    }
};

63.股票的最大利润
时间复杂度:O(n)
空间复杂度:
思路:最大利润就是数组中全部数对的最大差值。在卖出价固定时,买入价越低,得到的利润越大。
若是咱们在扫描到数组中的第 i 个数字时,只要咱们可以记住以前 i-1 个数字中的最小值,就能计算出在当前价位卖出时可能获得的最大利润。
核心:编程过程当中,最重要的是保存前面遍历过的值中的最小值。(这样只须要遍历一遍便可)


64.求1+2+3+…+n
时间复杂度:
空间复杂度:
思路:不太有实际价值


65.不用加减乘除作加法
代码
时间复杂度:
空间复杂度:O(1)
思路:三步走。
step1.两个数进行异或操做,这一步的目的是进行不进位的相加;
step2.两个数进行与操做,而且向右移动一位,这一步的目的是进行一位的进位操做;
step3.更新两个数,其中num1用第一步的结果更新,num2用第二步的结果更新;
不断循环,直到num2=0,即再也不有进位为止。


66.构建乘积数组(要求不能用除法)
代码
时间复杂度:O(n)
空间复杂度:O(n)
思路:
思路1.直观的暴力解,可是算法复杂度为O(n2); 思路2.采用“正三角和倒三角相乘”的方法。具体来讲,首先从上到下计算正三角的各行累乘结果,然后再从下到上计算倒三角的各行累乘结果,注意的是从始至终都只有一个vector,也就是最终返回的vector。