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\{\begin{matrix} 1\quad n=1 \\ 1 \quad n=2 \\ f(n-1)+f(n-2)\quad n>=3 \end{matrix}\right. \]

解法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\{\begin{matrix} 1\quad n=1 \\ 2\quad n=2 \\ f(n-1)+f(n-2) \end{matrix}\right. \]

旋转数组的最小数字

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

输入:

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]\times k[1]\times k[2]...\times k[m]\)最大乘积。

输入:

8

输出:

18

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

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

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

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

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

证明:当\(n\geq 5\)时,\(2\times (n-2)>n\)\(3\times(n-3)>n\),也即当绳子长度大于等于5时,应分成2或3的绳子段,又因为当\(n\geq 5\)时,\(2\times (n-2)\leq 3\times(n-3)\),也就是当绳子长度大于5时,应尽可能分成3的绳子段;\(n\leq 4\)时,\(1\times 3<2\times 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\{\begin{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}\right. \]

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

打印从1到最大的n位数

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

输入:

3

输出:

1
2
...
999

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

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

posted @ 2019-12-15 22:32  冬色  阅读(773)  评论(1编辑  收藏  举报