zoukankan      html  css  js  c++  java
  • LeetCode题解

    该文章对应的GitHub仓库:cnlinxi/algorithm_practise

    数组中重复的数字

    数组中所有数字都在0~n-1的范围内,数组中某些数字是重复的,找出重复的数字。如长度为7的数组{2, 3, 1, 0, 2 5, 3},对应的输出应为2或3.

    输入:

    2 3 1 0 2 5 3
    

    输出:

    2或者3
    
    • 解法1:排序,然后从前往后扫描数组,就可以找到重复数字。

      时间复杂度:(O(nlogn))

      空间复杂度:(O(1))

    • 解法2:哈希表统计每一个数字的出现频次,当一个数字出现频次大于1返回,C++可以使用std::map

      时间复杂度:(O(1))

      空间复杂度:(O(n))

    • 解法3:设数组名为numbers,让0~n-1每一个数字都放在其下标的位置上面,如果numbers[k]的位置上面没有放置k,就一直交换numbers[numbers[k]]和numbers[k],直到numbers[k]上面放置的是k。假设交换过程中发现numbers[k]与numbers[numbers[k]]相等,则重复数字就是numbers[k]。

      上例中,numbers[0]不是0,所以交换numbers[0]和numbers[numbers[0]],则变为:1 3 2 0 2 5 3;numbers[0]仍然不是0,交换numbers[0]和numbers[numbers[0]],则变为3 1 2 0 2 5 3;numbers[0]仍然不是0,变为0 1 2 3 2 5 3。此时numbers[0]变为0,进行下一个下标的检查,以保证每一个下标对应的值等于下标,直到循环到numbers[4]时,发现numbers[4] == numbers[numbers[4]],则重复数字就是numbers[4],返回即可。

      实际上这是一种求环的过程。

      时间复杂度:(O(n)),尽管有两重循环,但是每个数字最多只要交换两次就可以找到属于自己的位置。

      空间复杂度:(O(1))

      该方法修改了原始数组。

    • 解法4:所有数字都在[0,n-1]范围内,二分查找思想,不断缩小可能的重复数字范围,直到定位到重复数字。

      上例中,首先查找整个数组[0,3]范围内数字的个数,如果超过了4,则在[0,3]范围内一定有重复数字;下一步统计在[0,1]范围内数字个数为2,该范围一定没有重复数字,重复数字一定在[2,3]之间;下一步统计[2,2]范围内数字个数,超过了1,返回重复数字2。注意:二分查找的思想是每次撞大运numbers[mid]等于目标数字,相等就结束;但是该题结束条件是:直到搜索范围缩减到一个数字才可结束。

      时间复杂度:(O(nlogn)),二分查找(O(logn)),每次定一个范围之后,都要遍历这个数组一次。

      空间复杂度:(O(1))

    《剑指offer》面试题3

    二维数组中的查找

    从左往右,从上到下递增的二维数组,输入一个整数,判断二维数组是否存在该整数。

    输入:

    第一行:二维数组的行数,列数,要查找的数字,

    之后是这个二维数组。

    4 4 7
    1 2 8 9
    2 4 9 12
    4 7 10 13
    6 8 11 15
    

    输出:

    1
    

    解法:从这个数组右上角开始找,如果待查找的数字比矩阵中数字小,减小列;如果待查找的数字比矩阵中数字大,增大行。

    《剑指offer》面试题4

    替换空格

    将字符串中的空格替换为%20

    输入:

    We are happy.
    

    输出:

    We%20are%20happy.
    

    解法1:字符串替换,从后往前替换,否则替换一次都需要让后面的字符移动一次,时间复杂度变高。

    解法2:C++风格,扫描这个字符串,见到空格往std::vector放入%20,否则放入原来的字符,最后转化为std::string即可。

    从尾到头打印链表

    从链表的尾部向前打印链表。

    链表定义:

    struct ListNode {
        int m_nValue;
        ListNode *m_pNext;
    
        explicit ListNode(int x) : m_nValue(x), m_pNext(nullptr) {}
    };
    

    输入:

    [5,0,1,8,4,5]
    

    输出:

    5,4,8,1,0,5
    

    解法1:递归。如果节点为空,直接返回。否则先递归调用打印,然后打印该节点的值。

    解法2:利用栈,遍历链表时先存到栈里面,然后从栈里面拿出来打印。

    重建二叉树

    给定二叉树的前序和中序遍历结果,重建该二叉树。

    二叉树定义:

    struct TreeNode {
        int m_nValue;
        TreeNode *m_pLeft;
        TreeNode *m_pRight;
    
        explicit TreeNode(int x) : m_nValue(x), m_pLeft(nullptr), m_pRight(nullptr) {}
    };
    

    输入:

    [1,2,4,7,3,5,6,8]
    [4,7,2,1,5,3,8,6]
    

    输出(层次遍历,空节点输出null):

    [1,2,3,4,null,5,6,null,7,null,null,8,null,null,null,null,null]
    

    解法:

    前序遍历:N L R

    中序遍历:L N R

    在二叉树前序遍历的数组中,第一个数字就是当前根节点的值。在中序遍历的数组中,该根节点值的左侧是左子树,右侧是右子树,递归构建二叉树。

    二叉树的下一个节点

    给定二叉树和其中一个节点,返回中序遍历的下一个节点。

    输入(层次遍历,空节点输出null):

    [1,2,3,4,5,6,7,null,null,8,9,null,null,null,null,null,null,null,null]
    2
    

    输出:

    4
    

    解法:

       pNext2
       /
      /
    |pNode|
      
       s
    
    1. 如果该节点有右子树,则下一个节点为右子树的最左节点;
    2. 否则向上找,直到找到有一个节点是其父节点的左子节点,则下一个节点为该节点的父节点。

    用两个栈实现队列

    用两个栈实现队列。

    即实现以下声明:

    template<typename T>
    class CQueue {
    public:
        CQueue(void);
    
        ~CQueue(void);
    
        void appendTail(const T &node);
    
        T deleteHead();
    
    private:
        std::stack<T> stack1;
        std::stack<T> stack2;
    };
    

    解法:

    1. 入队:插入stack1;
    2. 出队:弹出stack2,如果stack2为空,则将stack1中元素转入stack2,然后弹出stack2。

    如入队1,2,3,出队1,2,入队4,5的情形:

    • 入队1,2,3

      stack1:
      | |
      |3|
      |2|
      |1|
      ---
      stack2:
      | |
      ---
      
    • 出队1,2

      • Step1:

        stack1:
        | |
        ---
        stack2:
        | |
        |1|
        |2|
        |3|
        ---
        
      • Step2:

        stack1:
        | |
        ---
        stack2:
        | |
        |3|
        ---
        
    • 入队4,5,6

      stack1:
      | |
      |6|
      |5|
      |4|
      ---
      stack2:
      | |
      |3|
      ---
      

    类似的,用两个队列实现栈:

    1. 入栈:将元素插入当前存储值的队列中,初始时随机插入一个队列中;
    2. 出栈:将队列中除了最后一个元素,全部出队移动到另一个队列中,最后元素直接丢弃。

    斐波那契数列

    输入n,求斐波那契数列第n项的值。

    输入:

    5
    

    输出:

    5
    

    解法:

    [f(n)=left{egin{matrix} 1quad n=1 \ 1 quad n=2 \ f(n-1)+f(n-2)quad n>=3 end{matrix} ight. ]

    解法1:递归。斐波那契数列的表达式明显是一个递归方程。

    解法2:从小n往大算。递归可以认为是从大n往小算,但是这种会带来计算重复。如计算(f(5))时,按照递归需要计算(f(4)+f(3)),而计算(f(4))又要计算(f(3)+f(2)),此时(f(3))就已经重复计算了。要减少这种重复计算,可以从小开始算,先计算(f(3)),再计算(f(4)),再计算(f(5)).

    解法3:此外,还有一种矩阵解法。

    类似的,跳台阶:

    有n级台阶,每次可以跳1级,也可以跳2级。求有多少种跳法。

    解法:实际也是斐波那契数列,设n级台阶有(f(n))种跳法,第一次跳1级则有(f(n-1))种跳法,第一次跳2级则有(f(n-2))种跳法,n级台阶的总跳法是这两种跳法之和。

    [f(n)=left{egin{matrix} 1quad n=1 \ 2quad n=2 \ f(n-1)+f(n-2) end{matrix} ight. ]

    旋转数组的最小数字

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

    输入:

    3 4 5 1 2
    

    输出:

    1
    

    解法1:从头到尾遍历数组,比较获得最小值。这种解法也不需要旋转递增数组,所有数组都可以这样做。

    解法2:二分查找的思想。一个指针初始化为0,始终指向前部的递增数组;另一个指针初始化为数组长度减1,始终指向后部的递增数组。中间位置的元素如果比前部指针指向的元素大,则中间位置在前部递增数组,前部指针向后移动;否则中间位置一定在后部递增数组中,后部指针向前移动,以逐步逼近最小元素位置,如果两个指针间隔相差1,则后一个指针指向的即是最小值。但该解法在数组有大量相同值时会失效,如:

    1 1 0 1 1
    

    初始化时,前后指针均指向相同值的元素,因此无法知道到底如何移动,这种情形只用从头到尾搜索最小值。

    矩阵中的路径

    判断在一个矩阵中是否存在一条包含字符串所有字符的路径。路径可以从矩阵中任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格,不可重复进入格子。

    输入:

    3 4
    a b t g
    c f c s
    j d e h
    bfce
    abfb
    

    第一行为矩阵的行列数,然后是该矩阵,之后是两个字符串,检查这两个字符串是否存在这个矩阵中。

    输出:

    1
    0
    

    解法:直接利用“回溯法”暴力搜索该路径。这种每一步可能有多种选择的,都可以使用回溯法,暴力搜索问题的解。

    素数环

    给定1到n数字,将数字依次填入环中,使得环中任意两个相邻的数字间的和为素数。对于给定的n,按字典序由小到大输出所有符合条件的解(第一个数字恒定为1)。

    输入:

    6
    8
    

    输出:

    Case 1:
    1 4 3 2 5 6
    1 6 5 2 3 4
    Case 2:
    1 2 3 8 5 6 7 4
    1 2 5 8 3 4 7 6
    1 4 7 6 5 8 3 2
    1 6 7 4 3 8 5 2
    

    解法:每一步有多种选择,使用回溯法暴力搜索问题的解,对于这种排列组合问题,回溯法很擅长。

    剪绳子

    给定长度n的绳子,剪成m段(m、n均为整数且n>1,m>1),记每段绳子的长度为(k[0],k[1],...,k[m]),求(k[0] imes k[1] imes k[2]... imes k[m])最大乘积。

    输入:

    8
    

    输出:

    18
    

    说明:当绳子长度为8时,剪成2、3、3三段时,乘积最长,最大乘积为(2 imes 3 imes 3=18).

    题解1:乘积因子不确定,因子个数也不确定。但实际上每个乘积“基团”最大时,整体的乘积也就最大,因而从整体上看,乘积因子就两个,保证这两个因子最大即可:

    [f(n)=mathop{max}(f(i) imes f(n-i)) ]

    这是一个从上至下的递归公式,但是递归会有很多的重复子问题,进而带来大量的重复计算。因此更好的办法是按照从下到上的顺序计算,也就是先计算(f(2),f(3)),再得到(f(4),f(5)),进而得到(f(n))

    题解2:可证,当(ngeq 5)时,尽可能多剪长度为3的绳子;当(n=4)时,应把绳子剪成两段长度为2的绳子。

    证明:当(ngeq 5)时,(2 imes (n-2)>n)(3 imes(n-3)>n),也即当绳子长度大于等于5时,应分成2或3的绳子段,又因为当(ngeq 5)时,(2 imes (n-2)leq 3 imes(n-3)),也就是当绳子长度大于5时,应尽可能分成3的绳子段;(nleq 4)时,(1 imes 3<2 imes 2),因此当(n=4)时,应该分为两段2的绳子段(实际长度等于4时,等于不用分了),其余的就不分了。

    二进制中1的个数

    输入一个整数,输出该数二进制中1的个数。

    输入:

    9
    

    输出:

    2
    

    说明:9的二进制为1001,其中有2位是1,因此输出2.

    解法1:用一个值为1的flag依次与该整数的每一位进行位与,每一次检查,左移flag一位。注意:必须对flag位移而不要对整数位移。这是因为移位之前如果整数是一个负数,仍然要保证移位后是一个负数,因此移位后的最高位会设为1,如果一直做右移运算,那么该整数会最终变为0xFFFFFFFF而陷入死循环。

    解法2: 将一个整数减去1,再和原整数做位与运算,会把该整数最右边的1变成0。因此可以检查一个整数可以做多少次这样的运算,就可以知道该整数到底有多少个1.

    数值的整数次方

    实现函数求base的exponent次方。不可使用库函数,也不可考虑大数问题。

    输入:

    2 2
    

    输出:

    4
    

    解法:如果一个一个乘起来会带来大量的重复计算,因此:

    [a^n=left{egin{matrix} a^{frac{n}{2}}cdot a^{frac{n}{2}} \ a^{frac{n-1}{2}}cdot a^{frac{n-1}{2}}cdot a end{matrix} ight. ]

    该公式是典型的递归形式。

    打印从1到最大的n位数

    输入数字n,按顺序打印从1到最大的n位十进制数。

    输入:

    3
    

    输出:

    1
    2
    ...
    999
    

    说明:3位数,应输出1到999.

    解法:大数,最常用的做法就是用字符串或者数组表达大数。全排列用递归很容易表达,数字的每一位都可能是0~9中的一个数,然后依次设置下一位,递归的结束条件是设置了数字的最后一位。其实就是平时用的回溯法,暴力搜索。

  • 相关阅读:
    常见算法:C语言求最小公倍数和最大公约数三种算法
    java数据结构
    创建与删除索引
    Delphi 2007体验!
    wxWindows
    Android中WebView的相关使用
    IAR FOR ARM 各版本号,须要的大家能够收藏了
    [AngularJS] $interval
    [ES6] 10. Array Comprehensions
    [ES6] 09. Destructuring Assignment -- 2
  • 原文地址:https://www.cnblogs.com/mengnan/p/12046388.html
Copyright © 2011-2022 走看看