剑指Offer【Python实现】

剑指Offer【Python实现】

目录

整理了两三天,终于整理出来剑指Offer的66道题了!

(一) 链表

二叉树中和为某一值的路径(一) 🍧

矩阵中的路径

题目描述:

​ 给定一个二叉树root和一个值 sum ,判断是否有从根节点到叶子节点的节点值之和等于 sum 的路径。

1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点

2.叶子节点是指没有子节点的节点

3.路径只能从父节点到子节点,不能从子节点到父节点

4.总节点数目为n

例如:
给出如下的二叉树, sum=22\ sum=22 sum=22,
img
返回true,因为存在一条路径 5→4→11→25\to 4\to 11\to 25→4→11→2的节点值之和为 22

数据范围:
1.树上的节点数满足 \(0 \leq n \leq 10000\)
2.每个节点的值都满足 \(|v a l| \leq 1000\)
要求: 空间复杂度 \(O(n)\), 时间复杂度 \(O(n)\)
进阶: 空间复杂度 \(O(\) 树的高度 \()\) ,时间复杂度 \(O(n)\)

输入描述:

{5,4,8,1,11,#,9,#,#,2,7},22

输出描述:

true

代码如下:

### 前序遍历,深度优先遍历dfs
class Solution(object):
    def __init__(self):
        self.result_all = []
        self.array = []

    def pathSum(self, root, expectNumber):
        if not root: return []
        self.array.append(root.val)
        expectNumber -= root.val
        if expectNumber == 0 and not root.left and not root.right:
            self.result_all.append(self.array[:])
        self.pathSum(root.left, expectNumber)
        self.pathSum(root.right, expectNumber)
        self.array.pop()
        return self.result_all

思路

先序遍历:

  • 每次访问一个节点,那么就将当前权值求和;
  • 如果当前权值和与期待的和一致,那么说明我们找到了一个路径,保存或者输出;
  • 每次深度遍历到底部,回退一个点。

复杂链表的复制 🍧

题目描述:

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。

img

示例:

输入:{1,2,3,4,5,3,5,#,2,#}

输出:{1,2,3,4,5,3,5,#,2,#}

解析:我们将链表分为两段,前半部分{1,2,3,4,5}为ListNode,后半部分{3,5,#,2,#}是随机指针域表示。

以上示例前半部分可以表示链表为的ListNode:1->2->3->4->5

后半部分,3,5,#,2,#分别的表示为

1的位置指向3,2的位置指向5,3的位置指向null,4的位置指向2,5的位置指向null

如下图:

img

输入描述:

{1,2,3,4,5,3,5,#,2,#}

输出描述:

{1,2,3,4,5,3,5,#,2,#}

代码如下:

class RandomListNode:
    def __init__(self, x):
        self.label = x
        self.next = None
        self.random = None


# 返回 RandomListNode
def Clone(pHead):
    # write code here
    if not pHead: return None
    p = pHead
    new_h = RandomListNode(p.label)
    pre_p = new_h
    random_map = {pHead: new_h}
    p = p.next
    while p:
        new_p = RandomListNode(p.label)
        random_map[p] = new_p
        pre_p.next = new_p
        pre_p = pre_p.next
        p = p.next
    p = pHead
    new_p = new_h
    while p:
        random_p = p.random
        if random_p:
            new_p.random = random_map[random_p]

        p = p.next
        new_p = new_p.next

    return new_h

p = RandomListNode(1)
p.next = RandomListNode(2)
p.next.next = RandomListNode(3)
p.next.next.next = RandomListNode(4)
p.next.next.next.next = RandomListNode(5)
print(Clone(p))

思路

二叉搜索树有:

  • 结点值:左<根<右
  • 左右子树都是搜索树

后序遍历顺序为:左->右->根

  • 最后一个数为根结点,通过根节点值切割左右子树。
  • 判断左右子树是否二叉搜索树

对于[4,8,6,12,16,14,10]

    10
 6     14  
4 8  12   16

两个链表的第一个公共结点 🍧

题目描述:

输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

数据范围: \(n \leq 1000\)
要求: 空间昌杂度 \(O(1)\) ,时间昌杂度 \(O(n)\)

例如,输入{1,2,3},{4,5},{6,7}时,两个无环的单向链表的结构如下图所示:

img

可以看到它们的第一个公共结点的结点值为6,所以返回结点值为6的结点。

输入描述:

输入分为是3段,第一段是第一个链表的非公共部分,第二段是第二个链表的非公共部分,第三段是第一个链表和二个链表的公共部分。 后台会将这3个参数组装为两个链表,并将这两个链表对应的头节点传入到函数FindFirstCommonNode里面,用户得到的输入只有pHead1和pHead2。

{1,2,3},{4,5},{6,7}

输出描述:

返回传入的pHead1和pHead2的第一个公共结点,后台会打印以该节点为头节点的链表。 

{6,7}

说明:

第一个参数{1,2,3}代表是第一个链表非公共部分,第二个参数{4,5}代表是第二个链表非公共部分,最后的{6,7}表示的是2个链表的公共部分
这3个参数最后在后台会组装成为2个两个无环的单链表,且是有公共节点的         

代码如下:

class Solution(object):
    def getIntersectionNode(self, pHead1, pHead2):
        p1, n_p1 = pHead1, 0
        p2, n_p2 = pHead2, 0
        while p1:
            p1 = p1.next
            n_p1 += 1
        while p2:
            p2 = p2.next
            n_p2 += 1
        if n_p1 < n_p2:     # p1指向长链
            pHead1, pHead2 = pHead2, pHead1
            n_p1, n_p2 = n_p2, n_p1

        for _ in range(n_p1 - n_p2):
            pHead1 = pHead1.next
        while pHead1 and pHead2:
            if pHead1 == pHead2:
                return pHead1
            else:
                pHead1 = pHead1.next
                pHead2 = pHead2.next
        return None

思路

倒序看,p1和p2两个链表的第一个公共结点到尾结点的长度一定相同。因此我们先对齐两个链表,再一起往后走找到第一个公共结点即可。

    1. 找出两个链表长度,n1和n2,长的链表先走n1-n2步。
    1. 一起往后走,找到第一个公共结点。

从尾到头打印链表 🍧

题目描述:

输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。

如输入{1,2,3}的链表如下图:

img

返回一个数组为[3,2,1]

0 <= 链表长度 <= 10000

输入描述:

{67,0,24,58}

输出描述:

[58,24,0,67]

代码如下:

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

# 解法1:用辅助栈存储
def printListFromTailToHead(listNode):
    # write code here
    stack = []
    result_array = []
    node_p = listNode
    while node_p:
        stack.append(node_p.val)
        node_p = node_p.next 
    while stack:
        result_array.append(stack.pop(-1))
    return result_array


# 解法2:本身栈调用
result_array = []
def printListFromTailToHead(listNode):
    # write code here
    if listNode:
        printListFromTailToHead(listNode.next)
        result_array.append(listNode.val)
    return result_array

反转链表 🍧

题目描述:

给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。

数据范围: \(n \leq 1000\) 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

如当输入链表{1,2,3}时,

经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。

以上转换过程如下图所示:

img

输入描述:

{1,2,3}

输出描述:

{3,2,1}

代码如下:

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

def ReverseList(pHead):
    # write code here

    if not pHead: return None
    head = ListNode(0)
    head.next = pHead
    p = pHead
    while(p.next):
        tp = p.next
        p.next = p.next.next
        tp.next = head.next
        head.next = tp
    return head.next

思路

假设翻转1->2->3->4->5,步骤如下:

head->4->3->2->1->5
               p  tp
  • 1.我们将p的next指向tp的next
  • 2.将tp的next指向head的next
  • 3.将head的next指向tp

合并有序链表 🍧

题目描述:

输入两个递增的链表,单个链表的长度为 \(n\) ,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: \(0 \leq n \leq 1000 ,-1000 \leq {\text { 节点值 } \leq 1000}{}\)
要求:空间㚉杂度 \(O(1) ,\) 时间复杂度 \(O(n)\)

如输入{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},所以对应的输出为{1,2,3,4,5,6},转换过程如下图所示:

img

或输入{-1,2,4},{1,3,4}时,合并后的链表为{-1,1,2,3,4,4},所以对应的输出为{-1,1,2,3,4,4},转换过程如下图所示:

img

输入描述:

{-1,2,4},{1,3,4}

输出描述:

{-1,1,2,3,4,4}

代码如下:

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

def Merge(pHead1, pHead2):
    # write code here
    res = ListNode(0)
    head = res
    while pHead1 and pHead2:
        if pHead1.val <= pHead2.val:
            head.next = pHead1
            head = head.next
            pHead1 = pHead1.next
        else:
            head.next = pHead2
            head = head.next
            pHead2 = pHead2.next
            
    if pHead1:
        head.next = pHead1
    if pHead2:
        head.next = pHead2
    return res.next

链表中倒数最后k个结点 🍧

题目描述:

输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。

如果该链表长度小于k,请返回一个长度为 0 的链表。

数据范围: \(0 \leq n \leq 10^{5} , 0 \leq a_{i} \leq 10^{9} , 0 \leq k \leq 10^{9}\)
要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)
进阶: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

例如输入{1,2,3,4,5},2时,对应的链表结构如下图所示:

img

其中蓝色部分为该链表的最后2个结点,所以返回倒数第2个结点(也即结点值为4的结点)即可,系统会打印后面所有的节点来比较。

输入描述:

{1,2,3,4,5},2

输出描述:

{4,5}
说明:
返回倒数第2个节点4,系统会打印后面所有的节点来比较。 

代码如下:

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


def FindKthToTail(head, k):
    # write code here
    if not head: return None
    fast_p = head
    slow_p = head
    for _ in range(k):
        if fast_p:
            fast_p = fast_p.next
        else:
            return None
    while fast_p:
        fast_p = fast_p.next
        slow_p = slow_p.next
    return slow_p

思路

用快慢指针,快指针比慢指针快k步,到尾结点了慢指针就是倒数第k个结点。

删除链表中重复的结点 🍧

题目描述:

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5

  • 数据范围: 链表长度满足 \(0 \leq n \leq 1000\) ,链表中的值满足 \(1 \leq\) val \(\leq 1000\)

  • 进阶:空间复杂度 \(O(n)\) ,时间夏杂度 \(O(n)\)

    例如输入{1,2,3,3,4,4,5}时,对应的输出为{1,2,5},对应的输入输出链表如下图所示:

    img

输入描述:

{1,2,3,3,4,4,5}

输出描述:

{1,2,5}

代码如下:

class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def deleteDuplication(self, pHead):
        # write code here
        head = ListNode(0)
        head.next = pHead
        pre = head
        p = head.next
        while p and p.next:
            if p.next.val == p.val:
                while p.next and p.next.val == p.val:
                    p.next = p.next.next
                pre.next = p.next
                p = pre.next
            else:
                pre = p
                p = p.next
        return head.next

(二) 树

二叉搜索树与双向链表 🍧

题目描述:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示

img
数据范围: 输入二叉树的节点数 \(0 \leq n \leq 1000\) ,二叉树中每个节点的值 \(0 \leq\) val \(\leq 1000\) 要求: 空间复杂度 \(O(1)\) (即在原树上操作),时间复杂度 \(O(n)\)

注意:

  1. 要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继;
  2. 返回链表中的第一个节点的指针;
  3. 函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构;
  4. 你不用输出双向链表,程序会根据你的返回值自动打印输出。

输入描述:

二叉树的根节点
{10,6,14,4,8,12,16}

输出描述:

双向链表的其中一个头节点。  
From left to right are:4,6,8,10,12,14,16;
From right to left are:16,14,12,10,8,6,4;
输入题面图中二叉树,输出的时候将双向链表的头节点返回即可。  

代码如下:

# -*- coding:utf-8 -*-
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
class Solution:
    def __init__(self):
        self.pre = None
        
    def Convert(self, root):
        # write code here
        if not root: return None
        self.helper(root)
        while root.left:
            root = root.left
        return root

    def helper(self, cur):
        if not cur: return None
        self.helper(cur.left)
        cur.left = self.pre
        if self.pre:
            self.pre.right = cur
        self.pre = cur
        self.helper(cur.right)

思路

二叉搜索树性质有:

  • 没有相同结点
  • 值:左<根<右

因此我们需要,中序遍历中:

  • pre.right = curr
  • curr.left = pre

树的子结构 🍧

题目描述:

输入两棵二叉树A,B,判断B是不是A的子结构。(我们约定空树不是任意一个树的子结构)

假如给定A为{8,8,7,9,2,#,#,#,#,4,7},B为{8,9,2},2个树的结构如下,可以看出B是A的子结构

img

数据范围:

0 <= A的节点个数 <= 10000

0 <= B的节点个数 <= 10000

输入描述:

{8,8,7,9,2,#,#,#,#,4,7},{8,9,2}

输出描述:

true

代码如下:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def helper(treeA, treeB):
    if not treeB:
        return True
    elif not treeA:
        return False
    elif treeA.val != treeB.val:
        return False
    else:
        return helper(treeA.left, treeB.left) and helper(treeA.right, treeB.right)


def HasSubtree(pRoot1, pRoot2):
    # write code here
    if not pRoot1 or not pRoot2: return False
    # 2 是不是 1的子树
    res = False
    if pRoot1.val == pRoot2.val:
        res = helper(pRoot1, pRoot2)
    if res: 
        return True
    else:
        return HasSubtree(pRoot1.left, pRoot2) or HasSubtree(pRoot1.right, pRoot2)

思路

    1. 遍历父结构
    1. 判断子结构是否相同

判断是不是平衡二叉树 🍧

题目描述:

输入一棵节点数为 n 二叉树,判断该二叉树是否是平衡二叉树。

在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

样例解释:

img

样例二叉树如图,为一颗平衡二叉树

注:我们约定空树是平衡二叉树。

  • 数据范围: \(n \leq 100\), 树上节点的val值满足 \(0 \leq n \leq 1000\)
  • 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

输入一棵二叉树的根节点
{1,2,3,4,5,6,7}

输出描述:

输出一个布尔类型的值
true

代码如下:

# 思路1
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def getDeep(x):
    if x is None:
        return 0
    return 1 + max(getDeep(x.left), getDeep(x.right))

class Solution:
    def IsBalanced_Solution(self, pRoot):
        # write code here
        if not pRoot:
            return 1
        return abs(getDeep(pRoot.left)-getDeep(pRoot.right)) <= 1 \
                and IsBalanced_Solution(pRoot.left) \
                and IsBalanced_Solution(pRoot.right) 
# 思路2
def getDeep(x):
    if x is None:
        return 0
    left = getDeep(x.left)
    if left == -1:
        return -1
    right = getDeep(x.right)
    if right == -1:
        return -1
    return -1 if abs(left-right) > 1 else 1 + max(left, right)


class Solution:
    def IsBalanced_Solution(self, pRoot):
        # write code here
        return getDeep(pRoot) != -1

思路

思路1:非剪枝

递归查看每棵子树是否满足平衡二叉树,o(nlogn)复杂度。

思路2:剪枝

看子树是否是平衡二叉树,如果不是返回-1,如果是返回长度。

二叉树的下一个结点 🍧

题目描述:

给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针。下图为一棵有9个节点的二叉树。树中从父节点指向子节点的指针用实线表示,从子节点指向父节点的用虚线表示

img

示例:

输入:{8,6,10,5,7,9,11},8

返回:9

解析:这个组装传入的子树根节点,其实就是整颗树,中序遍历{5,6,7,8,9,10,11},根节点8的下一个节点就是9,应该返回{9,10,11},后台只打印子树的下一个节点,所以只会打印9,如下图,其实都有指向左右孩子的指针,还有指向父节点的指针,下图没有画出来

img

  • 数据范围: 节点数满足 \(1 \leq n \leq 50\) ,节点上的值满足 \(1 \leq v a l \leq 100\)
  • 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

输入分为2段,第一段是整体的二叉树,第二段是给定二叉树节点的值,后台会将这2个参数组装为一个二叉树局部的子树传入到函数GetNext里面,用户得到的输入只有一个子树根节点。

{8,6,10,5,7,9,11},8

输出描述:

返回传入的子树根节点的下一个节点,后台会打印输出这个节点。
9

代码如下:

class TreeLinkNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None
        self.next = None

class Solution:
    def GetNext(self, pNode):
        # write code here
        if not pNode: return None
        p = pNode
        if pNode.right:  # 有右子树
            p = pNode.right
            while p.left:
                p = p.left
            return p
         # 没有右子树但是有父亲节点
        while pNode.next and pNode.next.right == pNode:
            pNode = pNode.next
        return pNode.next

对称的二叉树 🍧

题目描述:

给定一棵二叉树,判断其是否是自身的镜像(即:是否对称)
例如: 下面这棵二叉树是对称的
img
下面这棵二叉树不对称。
img

  • 数据范围: 节点数满足 \(0 \leq n \leq 1000\) ,节点上的值满足 \(\mid\) val \(\mid \leq 1000\)
  • 要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

备注:

你可以用递归和迭代两种方法解决这个问题

输入描述:

{1,2,2,3,4,4,3}

输出描述:

true

代码如下:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def isEqual(p1, p2):
    if not p1 and not p2:
        return True
    elif p1 and p2 and p1.val == p2.val:
        return True
    else:
        return False

class Solution:
    def isSymmetrical(self, pRoot):
        # write code here
        if not pRoot: return True
        queue = [pRoot]
        while queue:
            n = len(queue)
            if queue[0] != pRoot and n % 2 != 0: 
                return False
            for i in range(n//2):
                if not isEqual(queue[i], queue[-1-i]):
                    return False
            for _ in range(n):
                node = queue.pop(0)
                if not node: continue
                queue.append(node.left)
                queue.append(node.right)
        return True

思路

思路1. 正常的层序遍历

思路2. 两种中序遍历

    1. 左->根->右
    1. 右->根->左

按之字形顺序打印二叉树 🍧

题目描述:

给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)

  • 数据范围: \(0 \leq n \leq 1500\),树上每个节点的val满足 \(\mid\) val \(\mid<=100\)

  • 要求: 空间复杂度: \(O(n)\) ,时间复杂度: \(O(n)\)

例如:
给定的二叉树是{1,2,3,#,#,4,5}
img
该二叉树之字形层序遍历的结果是

[

[1],

[3,2],

[4,5]

]

输入描述:

{1,2,3,#,#,4,5}

说明:
如题面解释,第一层是根节点,从左到右打印结果,第二层从右到左,第三层从左到右。   

输出描述:

[[1],[3,2],[4,5]]

代码如下:

class Solution:
    def Print(self, pRoot):
        # write code here
        if not pRoot: return []
        res = []
        queue = [pRoot]
        j = -1
        while queue:
            j += 1
            n = len(queue)
            temp = []
            for _ in range(n):
                node = queue.pop(0)
                temp.append(node.val)
                if node.left: queue.append(node.left)
                if node.right: queue.append(node.right)
            if j % 2:
                temp.reverse()
            res.append(temp)
        return res

思路

  • 层序打印即可。

把二叉树打印成多行 🍧

题目描述:

给定一个节点数为 n 二叉树,要求从上到下按层打印二叉树的 val 值,同一层结点从左至右输出,每一层输出一行,将输出的结果存放到一个二维数组中返回。

例如:
给定的二叉树是{1,2,3,#,#,4,5}
img
该二叉树多行打印层序遍历的结果是

​ [

​ [1],

​ [2,3],

​ [4,5]

​ ]

  • 数据范围: 二叉树的节点数 \(0 \leq n \leq 1000 , 0 \leq\) val \(\leq 1000\)

  • 要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

输入描述:

给定一个二叉树的根节点
{1,2,3,#,#,4,5}

输出描述:

[[1],[2,3],[4,5]]

代码如下:

class Solution:
    # 返回二维列表[[1,2],[4,5]]
    def Print(self, pRoot):
        # write code here
        if not pRoot: return []
        queue = [pRoot]
        res = []
        while queue:
            n = len(queue)
            temp = []
            for _ in range(n):
                node = queue.pop(0)
                temp.append(node.val)
                if node.left: queue.append(node.left)
                if node.right: queue.append(node.right)
            res.append(temp)
        return res

二叉搜索树的第k个结点 🍧

题目描述:

给定一棵结点数为n 二叉搜索树,请找出其中的第 k 小的TreeNode结点值。

  1. 返回第k小的节点值即可

  2. 不能查找的情况,如二叉树为空,则返回-1,或者k大于n等等,也返回-1

  3. 保证n个节点的值不一样

  • 数据范围: \(0 \leq n \leq 1000 , 0 \leq k \leq 1000\) ,树上每个结点的值满足 \(0 \leq v a l \leq 1000\)

  • 进阶:空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

    如输入{5,3,7,2,4,6,8},3时,二叉树{5,3,7,2,4,6,8}如下图所示:

    img

    该二叉树所有节点按结点值升序排列后可得[2,3,4,5,6,7,8],所以第3个结点的结点值为4,故返回对应结点值为4的结点即可。

输入描述:

{5,3,7,2,4,6,8},3

输出描述:

4

代码如下:

class Solution:
    # 返回对应节点TreeNode
    
    def KthNode(self, pRoot, k):
        # write code here
        if not pRoot: return None
        stack = []
        while pRoot or stack:
            while pRoot:
                stack.append(pRoot)
                pRoot = pRoot.left
            pRoot = stack.pop()
            k -= 1
            if k == 0:
                return pRoot
            pRoot = pRoot.right

思路

  • 中序遍历第k个数。

重建二叉树 🍧

题目描述:

​ 给定节点数为 n 二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。

例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出如下图所示。

img

提示:

1.vin.length == pre.length

2.pre 和 vin 均无重复元素

3.vin出现的元素均出现在 pre里

4.只需要返回根结点,系统会自动输出整颗树做答案对比

数据范围: \(n \leq 2000\) ,节点的值 \(-10000 \leq\) val \(\leq 10000\) 要求:空间复杂度 \(O(n)\), 时间复杂度 \(O(n)\)

输入描述:

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

输出描述:

{1,2,3,4,#,5,6,#,7,#,#,8}

说明:
返回根节点,系统会输出整颗二叉树对比结果,重建结果如题面图示   

代码如下:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None


# 返回构造的TreeNode根节点
def reConstructBinaryTree(pre, tin):
    # write code here
    if not pre:
        return None
    root_val = pre[0]
    root = TreeNode(root_val)
    for i in range(len(tin)):
        if tin[i] == root_val:
            break
    root.left = reConstructBinaryTree(pre[1:1+i], tin[:i])
    root.right = reConstructBinaryTree(pre[1+i:], tin[i+1:])
    
    return root

def preorder(root):
    if root:
        preorder(root.left)
        print(root.val)
        preorder(root.right)
preorder(reConstructBinaryTree([1,2,3,4,5,6,7],[3,2,4,1,6,5,7]))

二叉树的镜像🍧

题目描述:

操作给定的二叉树,将其栾换为源二叉树的镜像。
数据范围: 二叉树的芇点数 \(0 \leq n \leq 1000\) ,茉点的值 \(0 \leq v a l \leq 1000\)
要求: 空间复杂度 \(O(n)\) 。本题也有原地操作,即空间复杂度 \(O(1)\) 的解法,时间复杂度 \(O(n)\)

比如:

源二叉树

img

镜像二叉树

img

输入描述:

{8,6,10,5,7,9,11}

输出描述:

{8,10,6,11,9,7,5}

说明:如题面所示    

代码如下:

def Mirror(root):
    # write code here
    if not root:
        return None
    tmp = Mirror(root.right)
    root.right = Mirror(root.left)
    root.left = tmp
    return root

二叉树的深度🍧

题目描述:

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度,根节点的深度视为 1 。

数据范围: 节点的数量满足 \(0 \leq n \leq 100\) ,节点上的值满足 \(0 \leq v a l \leq 100\)

进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

假如输入的用例为{1,2,3,4,5,#,6,#,#,7},那么如下图:

img

输入描述:

{1,2,3,4,5,#,6,#,#,7}

输出描述:

4

代码如下:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None


class Solution:
    def TreeDepth(self, pRoot):
        # write code here
        if not pRoot: return 0
        queue = [pRoot]
        deep = 0
        while queue:
            n = len(queue)
            for _ in range(n):
                node = queue.pop(0)
                if node.left: queue.append(node.left)
                if node.right: queue.append(node.right)
            deep += 1
        return deep

(三) 队列 & 栈

包含min函数的栈 🍧

题目描述:

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。

此栈包含的方法有:
push(value):将value压入栈中
pop():弹出栈顶元素
top():获取栈顶元素
min():获取栈中最小元素

数据范围: 操作数量满足 \(0 \leq n \leq 300\) ,输入的元綁满足 \(|v a l| \leq 10000\) 进阶:栈的各个操作的时间复杂度是 \(O(1)\) ,空间复杂度是 \(O(n)\)

示例:
输入: ["PSH-1","PSH2","MIN","TOP","POP","PSH1","TOP","MIN"]
输出: -1,2,1,-1
解析:
"PSH-1"表示将-1压入栈中,栈中元素为-1
"PSH2"表示将2压入栈中,栈中元素为2,-1
“MIN”表示获取此时栈中最小元素=>返回-1
"TOP"表示获取栈顶元素=>返回2
"POP"表示弹出栈顶元素,弹出2,栈中元素为-1
"PSH1"表示将1压入栈中,栈中元素为1,-1
"TOP"表示获取栈顶元素=>返回1
“MIN”表示获取此时栈中最小元素=>返回-1

输入描述:

["PSH-1","PSH2","MIN","TOP","POP","PSH1","TOP","MIN"]

输出描述:

-1,2,1,-1

代码如下:

class MinStack(object):

    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, node):
        # write code here
        self.stack.append(node)
        if not self.min_stack:
            self.min_stack.append(node)
        else:
            if self.min_stack[-1] < node:
                self.min_stack.append(self.min_stack[-1])
            else:
                self.min_stack.append(node)
    def pop(self):
        # write code here
        self.stack.pop(-1)
        self.min_stack.pop(-1)
    
    def top(self):
        # write code here
        if self.stack:
            return self.stack[-1]
        else:
            return []

    def min(self):
        # write code here
        return self.min_stack[-1] 

思路

  • 用辅助栈存储当前data的最小值,辅助栈头即为min值。

翻转单词序列🍧

题目描述:

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“nowcoder. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a nowcoder.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

数据范围: \(1 \leq n \leq 100\)
进阶:空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\) ,保证没有只包含空格的字符串

输入描述:

"nowcoder. a am I"

输出描述:

"I am a nowcoder."

代码如下:

class Solution:
    def ReverseSentence(self, s):
        # write code here
        stack = [n for n in s.split(' ')]
        stack.reverse()
        return ' '.join(stack)

print(Solution().ReverseSentence("I am a student."))

思路

考察翻转,可以通过空格隔开,然后用栈存储,最后弹出。

滑动窗口的最大值🍧

题目描述:

给定一个长度为 n 的数组 num 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。

例如,如果输入数组{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]

}。

窗口大于数组长度或窗口长度为0的时候,返回空。

  • 数据范围: \(1 \leq n \leq 10000,0 \leq s i z e \leq 10000\) ,数组中每个元傃的值满足 \(|v a l| \leq 10000\)

  • 要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

输入描述:

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

输出描述:

[4,4,6,6,6,5]

代码如下:

# -*- coding:utf-8 -*-
class Solution:
    def maxInWindows(self, num, size):
        # write code here
        if size == 0: return []
        queue = []
        res = []
        for i in range(len(num)):
            # 过期
            while queue and queue[0] <= i-size:
                queue.pop(0)
            # 挤走小数
            while queue and num[queue[-1]] < num[i]:
                queue.pop(-1)
            queue.append(i)
            if i < size - 1: 
                continue
            res.append(num[queue[0]])
        return res

思路

滑动窗口用队列保存,当遇到以下情况做相应处理:

    1. 窗口第一个槽始终是最大值
    1. 窗口第一个槽过期要丢弃
    1. 窗口从尾比到头,丢弃较小的数

栈的压入、弹出序列

题目描述:

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

  1. 0<=pushV.length == popV.length <=1000

  2. -1000<=pushV[i]<=1000

  3. pushV 的所有数字均不相同

输入描述:

[1,2,3,4,5],[4,3,5,1,2]

输出描述:

true

说明:
可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=>pop()=>pop()=>pop()=>pop()
这样的顺序得到[4,5,3,2,1]这个序列,返回true

代码如下:

class MinStack(object):

    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, node):
        # write code here
        self.stack.append(node)
        if not self.min_stack:
            self.min_stack.append(node)
        else:
            if self.min_stack[-1] < node:
                self.min_stack.append(self.min_stack[-1])
            else:
                self.min_stack.append(node)
    def pop(self):
        # write code here
        self.stack.pop(-1)
        self.min_stack.pop(-1)
    
    def top(self):
        # write code here
        if self.stack:
            return self.stack[-1]
        else:
            return []

    def min(self):
        # write code here
        return self.min_stack[-1] 

思路

  • 用辅助栈存储当前data的最小值,辅助栈头即为min值。

用两个栈实现队列 🍧

题目描述:

用两个栈来实现一个队列,使用n个元素来完成 n 次在队列尾部插入整数(push)和n次在队列头部删除整数(pop)的功能。 队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。

数据范围: \(n \leq 1000\) 要求: 存储 \(n\) 个元綁的空间复杂度为 \(O(n)\) ,揷入与删除的时间复杂度都是 \(O(1)\)

输入描述:

["PSH1","PSH2","POP","POP"]

输出描述:

1,2
说明:
"PSH1":代表将1插入队列尾部
"PSH2":代表将2插入队列尾部
"POP“:代表删除一个元素,先进先出=>返回1
"POP“:代表删除一个元素,先进先出=>返回2   

代码如下:

class CQueue(object):

    def __init__(self):
        self.stack1 = []
        self.stack2 = []

    def appendTail(self, value):
        self.stack1.append(value)
    
    def deleteHead(self):
        if not self.stack2:
            while self.stack1:
                self.stack2.append(self.stack1.pop(-1))
        if len(self.stack2) == 0: 
            return -1
        return self.stack2.pop(-1)
  • 思路
    一个栈用来存储,pop时弹出stack2,stack2为空,pop出stack1存储在stack2中。

(四) 搜索算法

字符串的排列🍧

题目描述:

输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。

例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。

img

  • 数据范围: \(n<10\)
  • 要求: 空间复杂度 \(O(n !)\) ,时间复杂度 \(O(n !)\)

输入描述:

"aab"

输出描述:

["aab","aba","baa"]

代码如下:

def helper(s):
    if len(s) == 1:
        return s[0]
    res = []
    for i in range(len(s)):
        l = helper(s[:i] + s[i+1:])
        for j in l:
            res.append(s[i] + j)

    return res

def Permutation(ss):
    # write code here
    if not ss: return []
    words = list(ss)
    return list(sorted(set(helper(words))))

print(Permutation('aa'))

思路

取出第i个数,全排列其他非i位置的数拼在后面。

二维数组中的查找 🍧

题目描述:

在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]

给定 target = 7,返回 true。
给定 target = 3,返回 false。

数据范围: 矩阵的长宽满足 \(0 \leq n, m \leq 500\) , 矩阵中的值满足 \(0 \leq v a l \leq 10^{9}\) 进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n+m)\)

输入描述:

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

输出描述:

true 

代码如下:

def find(target, array):
    i = 0
    j = len(array[0]) - 1
    while i < len(array) and j >= 0:
        base = array[i][j]
        if target == base:
            return True
        elif target > base: 
            i += 1
        else:
            j -= 1
    return False

print(find(4,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]))

数字在升序数组中出现的次数🍧

题目描述:

给定一个长度为 n 的非降序数组和一个非负数整数 k ,要求统计 k 在数组中出现的次数。

数据范围: \(0 \leq n \leq 1000,0 \leq k \leq 100\) ,数组中每个元表的值满足 \(0 \leq v a l \leq 100\) 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(\log n)\)

输入描述:

[1,2,3,3,3,3,4,5],3

输出描述:

4   

代码如下:

def biSearch(data, k):
    low = 0
    high = len(data) - 1
    while low <= high:
        mid = (low + high) // 2
        if data[mid] > k:
            high = mid - 1
        elif data[mid] < k:
            low = mid + 1
    return high

def GetNumberOfK(data, k):
    # write code here
    if not data: return 0
    return biSearch(data, k+0.5) - biSearch(data, k-0.5)

print(GetNumberOfK([3,3,3, 4],3))

思路

整体用二分法,找到头和尾。

因为data中都是整数,所以可以稍微变一下,不是搜索k的两个位置,而是搜索k-0.5和k+0.5

这两个数应该插入的位置,然后相减即可。

(五) 动态规划

正则表达式匹配🍧

题目描述:

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

数据范围:

  1. str 可能为空,且只包含从 a-z 的小写字母。

  2. pattern 可能为空,且只包含从 a-z 的小写字母以及字符 . 和 ,无连续的 ''。

    1. 0 <= str.length <= 20

    2. 0 <= pattern.length <= 30

要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

"aad","c*a*d"

输出描述:

true

说明:
因为这里 c 为 0 个,a被重复一次, * 表示零个或多个a。因此可以匹配字符串 "aad"。        

代码如下:

class Solution:
    # s, pattern都是字符串
    def match(self, s, pattern):
        # write code here
        s = '^' + s 
        pattern = '^' + pattern
        i = len(s) - 1
        j = len(pattern) - 1
        star_match = False
        while i>=0 and j>=0:
            if star_match:
                if pattern[j] == '.' or pattern[j] == s[i]:
                    if self.match(s[1:i], pattern[1:j]):  # 先靠前匹配,从1开始是去掉index为0的^
                        return True
                    else:
                        i -= 1
                else:
                    j -= 1
                    star_match = False
            else:
                if s[i] == pattern[j] or pattern[j] == '.':
                    i -= 1
                    j -= 1
                elif pattern[j] == '*':
                    star_match = True
                    j -= 1
                else:
                    return False
        return (i == -1 and j == -1)


print(Solution().match('aa', 'a*'))

思路

从后往前比较,递归判断,判断条件太多。

斐波那契数列🍧

题目描述:

大家都知道斐波那契数列,现在要求输入一个正整数 \(n\) ,请你输出斐波那契数列的第 \(n\) 项。 韭波那契数列是一个满足 \(f i b(x)=\left\{\begin{array}{rc}1 & x=1,2 \\ f i b(x-1)+f i b(x-2) & x>2\end{array}\right.\)

  • 数据范围: \(1 \leq n \leq 39\)
  • 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\) ,本题也有时间复杂度 \(O(\operatorname{logn})\) 的解法

输入描述:

一个正整数n
4

输出描述:

输出一个正整数。
3
根据斐波那契数列的定义可知,fib(1)=1,fib(2)=1,fib(3)=fib(3-1)+fib(3-2)=2,fib(4)=fib(4-1)+fib(4-2)=3,所以答案为4。 

代码如下:

def Fibonacci(n):
    # write code here
    f1 = 0
    f2 = 1
    if n == 0: return f1
    elif n == 1: return f2
    for _ in range(n-1):
        f2, f1 = f1+f2, f2
    return f2
print(Fibonacci(3))

跳台阶🍧

题目描述:

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

数据范围: \(0 \leq n \leq 40\)
要求: 时间复杂度: \(O(n)\) ,空间复杂度: \(O(1)\)

输入描述:

2

输出描述:

2

说明:
青蛙要跳上两级台阶有两种跳法,分别是:先跳一级,再跳一级或者直接跳两级。因此答案为2     

代码如下:

class Solution(object):
    def numWays(self, n):
        if n == 0: return 1
        f1 = 1
        f2 = 2
        if n == 1: return f1
        if n == 2: return f2
        for _ in range(n-2):
            f2, f1 = f1+f2, f2
        return f2 % 1000000007

思路

假设对于第n级台阶,总共有f(n)种跳法.

那么f(n) = f(n-1) + f(n-2),其中f(1)=1,f(2)=2

跳台阶扩展问题🍧

题目描述:

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。

数据范围: \(1 \leq n \leq 20\)
进阶:空间复杂度 \(O(1) ,\) 时间复杂度 \(O(1)\)

输入描述:

3

输出描述:

4

代码如下:

def jumpFloorII(number):
    # write code here
    f1 = 1
    if number == 1: return f1
    for _ in range(number-1):
        f1 = 2*f1
    return f1

思路

f(1)=1, f(2)=2, f(3)=4, f(4)=8 设n+1级f(n+1),有

f(n+1) = f(1) + f(2) + ... + f(n)

f(n+2) = f(1) + f(2) + ... + f(n+1)

f(n+2)= 2f(1) + 2f(2) + ... + 2f(n)

故得f(n+2) = 2f(n+1)

旋转数组的最小数字🍧

题目描述:

有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。

数据范围: \(1 \leq n \leq 10000\) ,数组中任意元龶的值: \(0 \leq\) val \(\leq 10000\) 要求: 空间复杂度: \(O(1)\) ,时间复杂度: \(O(\operatorname{logn})\)

输入描述:

[3,4,5,1,2]

输出描述:

1

代码如下:

class Solution(object):
    def minArray(self, numbers):
        low, high = 0, len(numbers) - 1
        while low < high:
            mid = (high+low) / 2
            if numbers[mid] > numbers[high]:
                low = mid + 1
            elif numbers[mid] < numbers[high]:
                high = mid
            else:
                if (numbers[high - 1] > numbers[high]):  # 确保正确的下标
                    low = high
                    break
                high -= 1  # 如果numbers[hign-1]=numbers[high]的情况
        return numbers[low]

思路

总体二分:

  • if mid大于high, low = mid - 1
  • if mid小于high, high = mid
  • 直到mid=high,取此位置的数

矩形覆盖🍧

题目描述:

我们可以用 21 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 21 的小矩形无重叠地覆盖一个 2*n 的大矩形,从同一个方向看总共有多少种不同的方法?

数据范围: \(0 \leq n \leq 38\)
进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

注意:约定 n == 0 时,输出 0

比如n=3时,2*3的矩形块有3种不同的覆盖方法(从同一个方向看):

img

输入描述:

//2*1的小矩形的总个数n

4

输出描述:

5

代码如下:

def rectCover(number):
    # write code here
    f1 = 1
    f2 = 2
    if number == 1: return f1
    if number == 2: return f2
    for _ in range(number-2):
        f2, f1 = f1 + f2, f2
    return f2

print(rectCover(3))

思路

f(1) = 1
f(2) = 2

f(n) = f(n-1) + f(n-2)

连续子数组的最大和🍧

题目描述:

输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

数据范围:

\[\begin{aligned} &1<=n<=10^{5} \\ &-100<=a[i]<=100 \end{aligned} \]

要求:时间复杂度为 \(O(n)\) ,空间复杂度为 \(O(n)\) 进阶:时间复杂度为 \(O(n)\) ,空间复杂度为 \(O(1)\)

输入描述:

[1,-2,3,10,-4,7,2,-5]

输出描述:

18
经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18   

代码如下:

def FindGreatestSumOfSubArray(array):
    # write code here
    max = array[0]
    dp = [0] * (len(array) + 1)
    dp[0] = array[0]
    for i in range(1, len(array)):
        if dp[i-1] < 0:
            dp[i] = array[i]
        else:
            dp[i] = array[i] + dp[i-1]
        if dp[i] > max:
            max = dp[i]
    return max

print(FindGreatestSumOfSubArray([6,-3,-2,7,-15,1,2,2]))

思路

  • 动态规划:

dp[i] = dp[i-1] + p[i] # if i != 0 and dp[i-1] > 0
dp[i] = p[i] # if i == 0 or dp[i-1] < 0

(六) 回溯

矩阵中的路径🍧

题目描述:

请设计一个函数,用来判断在一个n乘m的矩阵中是否存在一条包含某长度为len的字符串所有字符的路径。路径可以从矩阵中的任意
一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不
能再进入该格子。例如 \(\left[\begin{array}{llll}a & b & c & e \\ s & f & c & s \\ a & d & e & e\end{array}\right] \quad\) 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包

含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

  • 数据范围: \(0 \leq n, m \leq 20,1 \leq l e n \leq 25\)
  • 进阶: 时间复杂度 \(O\left(n^{2}\right)\), 空间复杂度 \(O\left(n^{2}\right)\)

输入描述:

[[a,b,c,e],[s,f,c,s],[a,d,e,e]],"abcced"

0 <= matrix.length <= 200
0 <= matrix[i].length <= 200

输出描述:

true

代码如下:

# -*- coding:utf-8 -*-
def DFS(matrix, row, col, path, visited):
    if row < 0 or row >= len(matrix) or col < 0 or col >= len(matrix[0]) or (row, col) in visited: 
        return False
    if path[0] == matrix[row][col]:
        if len(path) == 1:
            return True
        return DFS(matrix, row+1, col, path[1:], visited| {(row, col)}) or \
            DFS(matrix, row-1, col, path[1:], visited| {(row, col)}) or \
            DFS(matrix, row, col-1, path[1:], visited| {(row, col)}) or \
            DFS(matrix, row, col+1, path[1:], visited| {(row, col)})

class Solution:
    def hasPath(self, matrix, rows, cols, path):
        # write code here
        array = list(matrix)
        array = [array[i*cols:(i+1)*cols] for i in range(rows)]
        for i in range(rows):
            for j in range(cols):
                visited = set()
                if DFS(array, i, j, list(path), visited): 
                    return True
        return False

print(Solution().hasPath("ABCESFCSADEE",3,4,"BCCED"))
print(Solution().hasPath("ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS",5,8,"SLHECCEIDEJFGGFIE"))
print(Solution().hasPath("AAAAAAAAAAAA",3,4,"AAAAAAAAAAAA"))

思路

  • DFS+回溯

机器人的运动范围🍧

题目描述:

地上有一个 rows 行和 cols 列的方格。坐标从 [0,0] 到 [rows-1,cols-1] 。一个机器人从坐标 [0,0] 的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 threshold 的格子。 例如,当 threshold 为 18 时,机器人能够进入方格 [35,37] ,因为 3+5+3+7 = 18。但是,它不能进入方格 [35,38] ,因为 3+5+3+8 = 19 。请问该机器人能够达到多少个格子?

  • 数据范围: \(0 \leq\) threshold \(\leq 15 , 1 \leq\) rows, cols \(\leq 100\)
  • 进阶: 空间复杂度 \(O(\mathrm{~nm}) ,\) 时间复杂度 \(O(\mathrm{~nm})\)

输入描述:

# rows cols  
1,2,3

输出描述:

3

代码如下:

class Solution(object):
    def movingCount(self, m, n, k):
        """
        :type m: int
        :type n: int
        :type k: int
        :rtype: int
        """
        rows = m
        cols = n
        threshold = k
        array = []
        for i in range(rows):
            res = []
            for j in range(cols):
                res.append(getN(i, j))
            array.append(res)
        visited = [[0] * len(array[0]) for _ in range(len(array))]
        return BFS(array, 0, 0, threshold, visited)

def getN(i, j):
    res = 0
    while i:
        res += (i % 10)
        i //= 10
    while j:
        res += (j % 10)
        j //= 10
    return res

def BFS(array, i, j, threshold, visited):
    if i<0 or j<0 or i>len(array)-1 or j>len(array[0])-1 or array[i][j]>threshold or visited[i][j]:
        return 0
    res = 1
    visited[i][j] = 1
    res += BFS(array, i+1, j, threshold, visited)
    res += BFS(array, i-1, j, threshold, visited)
    res += BFS(array, i, j+1, threshold, visited)
    res += BFS(array, i, j-1, threshold, visited)
    return res

思路

BFS

(七) 排序

最小的K个数🍧

题目描述:

给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。

数据范围: \(0 \leq k, n \leq 10000\) ,数组中每个数的大小 \(0 \leq\) val \(\leq 1000\)
要求: 空间复杂度 \(O(n)\) ,时间嵏杂度 \(O(n \log n)\)

输入描述:

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

输出描述:

[1,2,3,4]

返回最小的4个数即可,返回[1,3,2,4]也可以        

代码如下:

#思路1
def sink(array, k):
    n = len(array)
    left = 2 * k + 1
    right = 2 * k + 2
    if left >= n: return
    min_i = left 
    if right < n and array[left] > array[right]:
        min_i = right
    if array[min_i] < array[k]:
        array[min_i], array[k] = array[k], array[min_i]
        sink(array, min_i)

def build_heap(list):
    n = len(list)
    for i in range(n//2, -1, -1):
        sink(list, i)

    return list

def GetLeastNumbers_Solution(tinput, k):
    if k > len(tinput): return []
    heap = build_heap(tinput)  # 建堆o(n)复杂度
    res = []
    for _ in range(k):  # 取topk o(klogn)复杂度
        heap[0], heap[-1] = heap[-1], heap[0]
        res.append(heap.pop())
        sink(heap, 0)
    return res


print(GetLeastNumbers_Solution([4,5,1,6,2,7,3,8],4))

#思路2
def partition(input, low, high):
    pivot = input[low]
    while low < high:
        while low < high and pivot <= input[high]:
            high -= 1
        input[low] = input[high]
        while low < high and pivot >= input[low]:
            low += 1
        input[high] = input[low]
    input[low] = pivot
    return low


def GetLeastNumbers_Solution(tinput, k):
    if k > len(tinput) or k <= 0: return []
    idx = partition(tinput, 0, len(tinput) - 1)
    low = 0
    high = len(tinput) - 1
    while idx != k - 1:
        if idx < k - 1:
            low = idx + 1
            idx = partition(tinput, low, high)
        if idx > k - 1:
            high = idx - 1
            idx = partition(tinput, low, high)
    return tinput[:k]


print(GetLeastNumbers_Solution([4,5,1,6,2,7,3,8],4))
print(GetLeastNumbers_Solution([-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8], 4))

思路

思路1:

  • 最小堆建立(需要o(n)时间复杂度)
  • 取出前k个数(每次取需要用logn时间重建堆)。时间复杂度为o(n)+o(k*logn)

思路2:

  • 用快排partition划分,一直划中第k个数 最差情况o(kn)。

数组中的逆序对🍧

题目描述:

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007

数据范围: 对于 \(50 \%\) 的数据, size \(\leq 10^{4}\)
对于 \(100 \%\) 的数据, size \(\leq 10^{5}\)
数组中所有数字的值满足 \(0 \leq v a l \leq 1000000\)
要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(\) nlogn \()\)

输入描述:

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

输出描述:

7  

代码如下:

def PrintFromTopToBottom(root):
    # write code here
    if not root: return []
    queue = [root]
    res = []
    while queue:
        n = len(queue)
        for _ in range(n):
            if not queue: break
            node = queue.pop(0)
            res.append(node.val)
            if node.left:             
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    return res

数据流中的中位数🍧

题目描述:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

  • 数据范围: 数据流中数个数满足 \(1 \leq n \leq 1000\) , 大小满足 \(1 \leq v a l \leq 1000\)
  • 进阶:空间复杂度 \(O(n)\) , 时间复杂度 \(O(n \log n)\)

输入描述:

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

输出描述:

"5.00 3.50 3.00 3.50 3.00 3.50 4.00 3.50 4.00 "

说明:
数据流里面不断吐出的是5,2,3...,则得到的平均数分别为5,(5+2)/2,3...   

代码如下:

class Solution:
    def __init__(self):
        self.sortedList = []
    def Insert(self, num):
        # write code here
        n = len(self.sortedList)
        self.sortedList.append(num)
        while n != 0:
            if self.sortedList[n] < self.sortedList[n-1]:
                self.sortedList[n], self.sortedList[n-1] = self.sortedList[n-1], self.sortedList[n]
            else:
                break
            n -= 1
        if n == 0:
            self.sortedList[n] = num

    def GetMedian(self, x):
        # write code here
        n = len(self.sortedList)
        if n % 2 == 0:
            return (self.sortedList[n//2]+self.sortedList[n//2-1]) / 2.0
        else:
            return self.sortedList[n//2]

数组中重复的数字🍧

题目描述:

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组[2,3,1,0,2,5,3],那么对应的输出是2或者3。存在不合法的输入的话输出-1。

  • 数据范围: \(0 \leq n \leq 10000\)
  • 进阶:时间莧杂度 \(O(n) ,\) 空间复杂度 \(O(n)\)

输入描述:

[2,3,1,0,2,5,3]

输出描述:

2

说明:
2或3都是对的   

代码如下:

class Solution:
    # 这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
    # 函数返回True/False
    def duplicate(self, numbers, duplication):
        # write code here
        map = [0] * 1000
        for n in numbers:
            if map[n] == 1:
                duplication[0] = n
                return True
            else:
                map[n] = 1
        return False

(八) 位运算

数值的整数次方🍧

题目描述:

实现函数 double Power(double base, int exponent),求base的exponent次方。

数据范围: \(\mid\) base \(|\leq 100,\), exponent \(\mid \leq 100\),保证最终结果一定满足 \(|v a l| \leq 10^{4}\) 进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

2.00000,-2

输出描述:

0.25000
2的-2次方等于1/4=0.25    

代码如下:

from ctypes import *
def NumberOf1(n):
    # write code here
    cnt = 0
    while c_int(n).value:
        n = n & (n-1)
        cnt += 1
        print(c_int(n), n)
    return cnt
print(NumberOf1(-3))

二进制中1的个数🍧

题目描述:

输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。

数据范围: \(-2^{31}<=n<=2^{31}-1\)
即范围为: \(-2147483648<=n<=2147483647\)

输入描述:

10

-1

输出描述:

2

32
说明:
1. 十进制中10的32位二进制表示为0000 0000 0000 0000 0000 0000 0000 1010,其中有两个1。  

2. 负数使用补码表示 ,-1的32位二进制表示为1111 1111 1111 1111 1111 1111 1111 1111,其中32个1

代码如下:

def Power(base, exponent):
    # write code here
    if exponent == 0: return 1
    flag = 1
    if exponent < 0:
        exponent *= -1
        flag = 0
    temp = base
    res = 1
    while(exponent):
        if exponent & 1:
            res *= temp
        temp *= temp
        exponent = exponent >> 1

    return res if flag else 1.0/res

print(Power(2, 1))

思路

如果n!=0,n的二进制中至少有一个1

  • 如果1在最低位,n-1 & n 得到的数正好将这个1,变成0。
  • 如果1不在最低位,n-1 & n 得到的数正好将这个1,变成0。

因此我们判断n-1 & n 能够循环运行的次数就可以判断二进制中有多少个1了。

在python中需要使用c_int()函数,不然负数不会变成0。

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

题目描述:

一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

数据范围: 数组长度 \(2 \leq n \leq 1000\) ,数组中每个数的大小 \(0<v a l \leq 1000000\) 要求:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

提示:输出时按非降序排列。

输入描述:

[1,4,1,6]

输出描述:

[4,6]
返回的结果中较小的数排在前面

代码如下:

class Solution:
    # 返回[a,b] 其中ab是出现一次的两个数字
    def FindNumsAppearOnce(self, array):
        # write code here
        map = {}
        res = []
        for n in array:
            map[n] = map.get(n, 0) + 1
        for n in array:
            if map[n] == 1:
                res.append(n)
        return res

思路

位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身。感觉意义不大,不看了。

求1+2+3+...+n🍧

题目描述:

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

数据范围: \(0<n \leq 200\)
进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

5

输出描述:

15

代码如下:

def sum(n):
    try:
        1 % n
        return n + sum(n-1)
    except:
        return 0


class Solution:
    def Sum_Solution(self, n):
        return sum(n)


print(Solution().Sum_Solution(10))

思路

  • 短路思想感觉太偏了,工程上可以用try catch

不用加减乘除做加法🍧

题目描述:

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

  • 数据范围: 两个数都满足 \(0 \leq n \leq 1000\)

  • 进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(1)\)

输入描述:

1,2

输出描述:

3

代码如下:

from ctypes import *

class Solution:
    def Add(self, num1, num2):
        # write code here
        while num2 != 0:
            temp = c_int(num1 ^ num2).value  # 不带进位的相加结果
            num2 = c_int((num1 & num2) << 1).value  # 带进位
            num1 = temp
        return num1

print(Solution().Add(-1, 23))

思路

参考链接:https://www.nowcoder.com/questionTerminal/59ac416b4b944300b617d4f7f111b215

首先看十进制是如何做的: 5+7=12,三步走
第一步:相加各位的值,不算进位,得到2。
第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。

第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。

同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。

第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。

第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。

(九) 模拟

扑克牌顺子🍧

题目描述:

现在有2副扑克牌,从扑克牌中随机五张扑克牌,我们需要来判断一下是不是顺子。
有如下规则:

  1. A为1,J为11,Q为12,K为13,A不能视为14。

  2. 大、小王为 0,0可以看作任意牌。

  3. 如果给出的五张牌能组成顺子(即这五张牌是连续的)就输出true,否则就输出false。

  4. 数据保证每组5个数字,每组最多含有4个零,数组的数取值为 [0, 13]。

要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n \log n)\) ,本题也有时间复杂度 \(O(n)\) 的解法。

输入描述:

输入五张扑克牌的值

[6,0,2,0,4]

输出描述:

五张扑克牌能否组成顺子。

true

说明:
中间的两个0一个看作3,一个看作5 。即:[6,3,2,5,4]
这样这五张牌在[2,6]区间连续,输出true 

代码如下:

class Solution:
    def IsContinuous(self, numbers):
        # write code here
        if not numbers: return 0
        numbers.sort()
        cnt = 0
        jokers = 0
        pre = None
        for i in range(len(numbers)):
            if numbers[i] == 0:
                jokers += 1
            else:
                if pre is None:
                    pre = numbers[i]
                else:
                    if pre == numbers[i]: return 0
                    cnt += numbers[i] - pre - 1
                    pre = numbers[i]
        cnt -= jokers
        return cnt <= 0

print(Solution().IsContinuous([0,3,1,6,4]))

思路

思路1:

    1. 排序
    1. 计算顺差

思路2:

  • 1.最大值-最小值 < 5
  • 2.不能有对
  • 3.必须有牌

把字符串转换成整数(atoi)🍧

题目描述:

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。传入的字符串可能有以下部分组成:

1.若干空格

2.(可选)一个符号字符('+' 或 '-')

  3. 数字,字母,符号,空格组成的字符串表达式  

  4. 若干空格   

转换算法如下:
1.去掉无用的前导空格
2.第一个非空字符为+或者-号时,作为该整数的正负号,如果没有符号,默认为正数
3.判断整数的有效部分:
3.1 确定符号位之后,与之后面尽可能多的连续数字组合起来成为有效整数数字,如果没有有效的整数部分,那么直接返回0
3.2 将字符串前面的整数部分取出,后面可能会存在存在多余的字符(字母,符号,空格等),这些字符可以被忽略,它们对于函数不应该造成影响
**3.3 整数超过 32 位有符号整数范围 [−231, 231-1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231的整数应该被调整为 −231,大于231− 1 的整数应该被调整为 231− 1。
**4. 去掉无用的后导空格。

数据范围:

1.0 <=字符串长度<= 100

2.字符串由英文字母(大写和小写)、数字(0-9)、' '、'+'、'-' 和 '.' 组成

输入描述:

"4396 clearlove"

输出描述:

4396

说明:
去掉前后的空格,为-12  

代码如下:

class Solution:
    def StrToInt(self, s):
        # write code here
        res = 0
        flag = 1
        for i in range(len(s)):
            if i == 0 and s[i] == '+':
                continue
            elif i == 0 and s[i] == '-':
                flag = -1
                continue
            n = ord(s[i]) - ord('0')
            if n>=0 and n<=9:
                res = 10 * res + n
            else:
                return False
        return res * flag

print(Solution().StrToInt('-1234'))

表示数值的字符串🍧

题目描述:

请实现一个函数用来判断字符串str是否表示数值(包括科学计数法的数字,小数和整数)。

科学计数法的数字(按顺序)可以分成以下几个部分:

1.若干空格

2.一个整数或者小数

3.(可选)一个 'e' 或 'E' ,后面跟着一个整数(可正可负)

4.若干空格

小数(按顺序)可以分成以下几个部分:

  1. 若干空格

  2. 可选)一个符号字符('+' 或 '-')

  3. 可能是以下描述格式之一:

  • 3.1 至少一位数字,后面跟着一个点 '.'

  • 3.2 至少一位数字,后面跟着一个点 '.' ,后面再跟着至少一位数字

  • 3.3 一个点 '.' ,后面跟着至少一位数字

  1. 若干空格

整数(按顺序)可以分成以下几个部分:

  1. 若干空格

  2. 一个符号字符('+' 或 '-')

  3. 至少一位数字

  4. 若干空格

例如,字符串["+100","5e2","-123","3.1416","-1E-16"]都表示数值。

但是["12e","1a3.14","1.2.3","+-5","12e+4.3"]都不是数值。

提示:

  1. 1 <= str.length <= 25

  2. str 仅含英文字母(大写和小写),数字(0-9),加号 '+' ,减号 '-' ,空格 ' ' 或者点 '.' 。

  3. 如果怀疑用例是不是能表示为数值的,可以使用python的print(float(str))去查看。

进阶:时间复杂度 \(O(n)\) ,空间复杂度 \(O(n)\)

输入描述:

"123.45e+6"

输出描述:

true

代码如下:

class Solution:
    # s字符串
    def isNumeric(self, s):
        # write code here
        sign, point, hasE = False, False, False
        for i in range(len(s)):
            if s[i].lower() == 'e':
                if hasE: return False
                if i == len(s)-1: return False
                hasE = True
            elif s[i] == '+' or s[i] == '-':
                if sign and s[i-1].lower() != 'e': return False
                if not sign and i > 0 and s[i-1].lower() != 'e': return False
                sign = True
            elif s[i] == '.':
                if hasE or point: return False
                point = True
            elif ord(s[i]) < ord('0') or ord(s[i]) > ord('9'):
                return False
        return True

顺时针打印矩阵🍧

题目描述:

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵:

[[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16]]

则依次打印出数字

[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]

数据范围:
0 <= matrix.length <= 100
0 <= matrix[i].length <= 100

输入描述:

[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]

输出描述:

[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10] 

代码如下:

def printMatrix(matrix):
    # write code here
    walked = [[False] * (len(matrix[0])+1) for _ in range(len(matrix)+1)]
    for j in range(len(walked[-1])):
        walked[-1][j] = True
    for i in range(len(walked)):
        walked[i][-1] = True
    len_row = len(matrix) - 1
    len_col = len(matrix[0]) - 1
    res = []
    i = 0
    j = 0
    direction = 0  # 0向右,1向下,2向左,3向上

    while not walked[i][j]:
        res.append(matrix[i][j])
        walked[i][j] = True
        if direction == 0: # right
            if j < len_col and not walked[i][j+1]:
                j += 1
            else:
                direction = 1
                i += 1
        elif direction == 1: # down
            if i < len_row and not walked[i+1][j]:
                i += 1
            else:
                direction = 2
                j -= 1
        elif direction == 2:  # left
            if j > 0 and not walked[i][j-1]:
                j -= 1
            else:
                direction = 3
                i -= 1
        elif direction == 3:  # up
            if i > 0 and not walked[i-1][j]:
                i -= 1
            else:
                direction = 0
                j += 1
    return res

print(printMatrix([[1,2],[3,4]]))
print(printMatrix([[1]]))

思路

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16

  • 用一个bool数组标记走过的路,到底了就按照 右→下→左→上→右 的方式修改运动方向。

(十) 其他算法

丑数🍧

题目描述:

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第 n个丑数。

数据范围: \(0 \leq n \leq 2000\)
要求: 空间复杂度 \(O(n)\) , 时间复杂度 \(O(n)\)

输入描述:

7

输出描述:

8 

代码如下:

# 1 2 3 4 5 6 8

# def is_ugly(n):
#     while n % 2 == 0:
#         n /= 2
#     while n % 3 == 0:
#         n /= 3
#     while n % 5 == 0:
#         n /= 5
#     return n == 1

# 思路2
def GetUglyNumber_Solution(index):
    # write code here
    if index <= 0: return 0
    if index == 1: return 1
    uglys = {1}
    cnt = 0
    i = 0
    while cnt < index-1:
        i += 1
        if i/2.0 in uglys or i/3.0 in uglys or i/5.0 in uglys:
            cnt += 1
            uglys.add(i)

    return i

print(GetUglyNumber_Solution(700))

# 思路3
def GetUglyNumber_Solution(index):
    # write code here
    if index <= 1: return index
    res = [1] * index
    i2, i3, i5 = 0, 0, 0
    for i in range(1, index):
        res[i] = min(res[i2]*2, min(res[i3]*3, res[i5]*5))
        if res[i] == res[i2]*2: i2 += 1
        if res[i] == res[i3]*3: i3 += 1
        if res[i] == res[i5]*5: i5 += 1
    return res[-1]


print(GetUglyNumber_Solution(700))

思路

  • 思路1:暴力法

需要遍历所有数,o(n^2)复杂度,超时。

  • 思路2:暴力+存储

也需要遍历所有数,只是反证丑数时节约时间,o(n^2)复杂度,依旧超时。

  • 思路3:
    存储最小丑数,依次向上取,o(n)复杂度。

替换空格🍧

题目描述:

请实现一个函数,将一个字符串s中的每个空格替换成“%20”。

例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

数据范围: \(0 \leq \operatorname{len}(s) \leq 1000\) 。保证字符串中的字符为大写英文字母、小写英文字母和空格中的一种。 进阶:时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)

输入描述:

"We Are Happy"

" "

输出描述:

"We%20Are%20Happy"

"%20"

代码如下:

def replaceSpace(s):
    # write code here
    s_len = len(s)
    space_count = 0
    for i in s:
        if i == ' ':
            space_count += 1
    s_len += 2 * space_count
    new_array = [' '] * s_len
    j = 0
    for i in range(len(s)):
        if s[i] == ' ':
            new_array[j] = '%'
            new_array[j+1] = '2'
            new_array[j+2] = '0'
            j += 3
        else:
            new_array[j] = s[i]
            j += 1
    return ''.join(new_array)
print(replaceSpace('We Are Happy'))

字符流中第一个不重复的字符🍧

题目描述:

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g" 。当从该字符流中读出前六个字符 “google" 时,第一个只出现一次的字符是"l"。

  • 数据范围: 字符串长度满足 \(1 \leq n \leq 1000\) ,字符串中出现的字符一定在 \(ASCll\) 码内。
  • 进阶:空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

后台会用以下方式调用 Insert 和 FirstAppearingOnce 函数。

string caseout = "";

1.读入测试用例字符串casein

2.如果对应语言有Init()函数的话,执行Init() 函数

3.循环遍历字符串里的每一个字符ch {

Insert(ch);

caseout += FirstAppearingOnce()

}

  1. 输出caseout,进行比较

输入描述:

"google"

输出描述:

"ggg#ll"

如果当前字符流没有存在出现一次的字符,返回#字符。

代码如下:

class Solution:
    # 返回对应char
    def __init__(self):
        self.data = []
    def FirstAppearingOnce(self):
        # write code here
        return self.data[0] if self.data else '#'
    def Insert(self, char):
        # write code here
        n = len(self.data)
        for i in range(n-1, -1, -1):
            if self.data[i] == char:
                self.data.pop(i)
                return
        self.data.append(char)


s = Solution()
print(s.Insert('g'))
print(s.Insert('o'))
print(s.Insert('o'))
print(s.Insert('g'))
print(s.Insert('l'))
print(s.Insert('e'))

第一个只出现一次的字符🍧

题目描述:

在一个长为 字符串中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)

数据范围: \(0 \leq n \leq 10000\) ,且字符串只有字母组成。 要求: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

输入描述:

"google"

输出描述:

4  

代码如下:

def FirstNotRepeatingChar(s):
    # write code here
    map = {}
    for i in range(len(s)):
        map[s[i]] = map.get(s[i], 0) + 1
    for i in range(len(s)):
        if map[s[i]] == 1:
            return i
    return -1

print(FirstNotRepeatingChar('abac'))

思路

  • 反向遍历,哈希表存储字母数量,最后一个数量为1的字母为最终的输出。

调整数组顺序使奇数位于偶数前面(一)🍧

题目描述:

输入一个长度为 n 整数数组,数组里面不含有相同的元素,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前面部分,所有的偶数位于数组的后面部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

数据范围: \(0 \leq n \leq 5000\) ,数组中每个数的值 \(0 \leq\) val \(\leq 10000\)
要求: 时间复杂度 \(O(n)\) ,空间复杂度 \(O(n)\)
进阶:时间复杂度 \(O\left(n^{2}\right)\) ,空间复杂度 \(O(1)\)

输入描述:

[2,4,6,5,7]

输出描述:

[5,7,2,4,6]

代码如下:

def reOrderArray(array):
    # write code here
    odd_cnt = 0
    res = [0] * len(array)
    # 统计个数
    for n in array:  
        if n % 2 != 0:
            odd_cnt += 1
    # 填坑
    odd_i = 0
    even_i = odd_cnt
    for i in range(len(array)):
        if array[i] % 2 != 0:  
            res[odd_i] = array[i]
            odd_i += 1
        else:
            res[even_i] = array[i]
            even_i += 1
    return res

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

题目描述:

给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

例如输入一个长度为9的数组[1,2,3,2,2,2,5,4,2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

数据范围: \(n \leq 50000\) ,数组中元綁的值 \(0 \leq v a l \leq 10000\) 要求: 空间复杂度: \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

保证数组输入非空,且保证有解

[1,2,3,2,2,2,5,4,2]

输出描述:

2

代码如下:

def MoreThanHalfNum_Solution(numbers):
    # write code here
    res = None
    cnt = 0
    for i in numbers:  # 留下数组中出现次数最高的数
        if not res:
            res = i
            cnt = 1
        else:
            if i == res:
                cnt += 1
            else:
                cnt -= 1
                if cnt == 0:
                    res = None
    # 判断次数是否大于一半
    cnt = 0 
    for i in numbers:
        if i == res:
            cnt += 1
    if cnt > len(numbers) / 2:
        return res
    else:
        return 0

print(MoreThanHalfNum_Solution([1,2,3,2,2,2,5,4,2]))

思路

  • 第一步:记录数组中次数出现最多的个数
  • 第二步:判断这个数是否出现次数超过一半

整数中1出现的次数(从1到n整数中1出现的次数)🍧

题目描述:

输入一个整数 n ,求 1~n 这 n 个整数的十进制表示中 1 出现的次数;
例如, 1~13 中包含 1 的数字有 1 、 10 、 11 、 12 、 13 因此共出现 6 次。

注意:11 这种情况算两次

数据范围: \(1 \leq n \leq 30000\)
进阶:空间复杂度 \(O(1)\) ,时间复杂度 \(O(\log n n)\)

输入描述:

13

输出描述:

6

代码如下:

def NumberOf1Between1AndN_Solution(n):
    # write code here
    res = 0
    i = 1  # 个位开始
    while n // i != 0:
        high = n//(i*10) # 高位数
        current = (n//i) % 10  # 第i位数
        low = n - (n//i) * i  # 低位数
        if current == 0:
            res += high * i
        elif current == 1:
            res += high * i + low + 1
        else:
            res += (high+1) * i
        i *= 10
    return res

print(NumberOf1Between1AndN_Solution(20))

思路

以百位为例:

  • 百位数为0,情况由更高位决定,10022,百位数字出现1的情况:100-199,1100-1199,2100-2199,...9100-9199,共100*10=1000种。即高位(10) * 位置(100)
  • 百位数为1,情况由更高位和百位决定,10122,百位数字出现1的情况为:100-199, 1100-1199, 2100-2199,...9100-9199,10100-10122,共1000+23种。即高位(10) * 位置(100) + 低位(23)
  • 百位数大于1,10222,百位数出现1的情况为: 100-199, 1100-1199, ...9100-9199, 10100-10199 共1100种。即(高位(10)+1) * 位置(100)

最终统计所有位置可能的1的情况。时间复杂度O(logn)

把数组排成最小的数🍧

题目描述:

输入一个非负整数数组numbers,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

例如输入数组[3,32,321],则打印出这三个数字能排成的最小数字为321323。

1.输出结果可能非常大,所以你需要返回一个字符串而不是整数
2.拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

数据范围:

0<=len(numbers)<=100

输入描述:

[11,3]

输出描述:

"113"

代码如下:

# 思路1
def number_len(n):
    res = 0
    while n:
        n //= 10
        res += 1
    return res

def PrintMinNumber(numbers):
    # write code here
    if len(numbers) == 1: return numbers[0]
    max_len = number_len(max(numbers))
    map = {}
    for i in range(len(numbers)):
        n = numbers[i]
        pad = n % 10
        n_len = number_len(n)
        for j in range(max_len-n_len):
            n = n*10+pad
        map[n*10+n_len] = i
    keys = sorted(map.keys())
    res = numbers[map[keys[0]]]
    for i in range(1, len(keys)):
        n = numbers[map[keys[i]]]
        res = res * 10 ** number_len(n) + n
    return res

    # map = {numbers[i]:i for i in range(len(numbers))}

print(PrintMinNumber([11,1,111]))

# 思路2
import functools
def compare(s1, s2):
    if s1+s2 < s2+s1:
        return -1
    elif s1+s2 == s2+s1:
        return 0
    else:
        return 1

def PrintMinNumber(numbers):
    # write code here
    if not numbers: return ''
    if len(numbers) == 1: return numbers[0]
    str_numbers = [str(n) for n in numbers]

    return ''.join(sorted(str_numbers, key=functools.cmp_to_key(compare)))

print(PrintMinNumber([32, 3, 321]))

思路

本题主要是找排序方法判断所有数的顺序方法。

思路1

  • 1.全部用个位数+长度补齐,比如{3, 32, 321}补齐成{3331, 3222, 3213},{1, 11, 111}补齐成{1111, 1112, 1113} o(nlogn)
  • 2.排序,从小到大取下标 o(nlogn)
  • 3.根据下标的数组装结果

思路2

  • 1.比较n1与n2,转成字符串s1与s2
  • 2.比较s1s2与s2s1大小,小的放前面
  • 3.依次输出所有字符串

和为S的连续正数序列🍧

题目描述:

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?

数据范围: \(0<n \leq 100\)
进阶:时间复杂度 \(O(n)\)

返回值描述:

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序。

输入描述:

9

输出描述:

[[2,3,4],[4,5]]  

代码如下:

class Solution:
    def FindContinuousSequence(self, tsum):
        # write code here
        res = []
        windows = []
        sum = 0
        for t in range(1, tsum):
            windows.append(t)
            sum += t
            while sum > tsum:
                sum -= windows.pop(0)
            if sum == tsum:
                res.append(windows[:])

        return res    

print(Solution().FindContinuousSequence(100))

思路

用滑动窗口抓取

和为S的两个数字🍧

题目描述:

输入一个递增排序的数组array和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,返回任意一组即可,如果无法找出这样的数字,返回一个空数组即可。

​ 数据范围:

0<=len(array) <=105

1<=array[i] <=106

输入描述:

[1,2,4,7,11,15],15

输出描述:

[4,11]

说明:
返回[4,11]或者[11,4]都是可以的      

代码如下:

class Solution:
    def FindNumbersWithSum(self, array, tsum):
        # write code here
        if not array: return []
        lp = 0
        rp = len(array) - 1
        res = [array[-1]] * 2
        while lp < rp:
            tmp = array[lp] + array[rp]
            if tmp > tsum:
                rp -= 1
            elif tmp < tsum:
                lp += 1
            else:
                if array[lp] * array[rp] < res[0] * res[1]:
                    res[0], res[1] = array[lp], array[rp]
                lp += 1
                rp -= 1

        return res if res[0] + res[1] == tsum else []

print(Solution().FindNumbersWithSum([], 0))

思路

左右指针,和大于target,右指针左移,和小于target,左指针右移。

左旋转字符串🍧

题目描述:

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列 S ,请你把其循环左移 K 位后的序列输出。例如,字符序列 S = ”abcXYZdef” , 要求输出循环左移 3 位后的结果,即 “XYZdefabc” 。是不是很简单?OK,搞定它!

数据范围: 输入的字符串长度满足 \(0 \leq\) len \(\leq 100 , 0 \leq n \leq 100\)

进阶: 空间复杂度 \(O(n)\) ,时间复杂度 \(O(n)\)

输入描述:

"abcXYZdef",3

输出描述:

"XYZdefabc"

代码如下:

def reverse(str, s, e):
    e -= 1
    while s < e:
        str[s], str[e] = str[e], str[s]
        s += 1
        e -= 1

class Solution:
    def LeftRotateString(self, s, n):
        # write code here
        if len(s) == 0 or n == 0: return s
        s = list(s)
        reverse(s, 0, n)
        reverse(s, n, len(s))
        reverse(s, 0, len(s))
        return ''.join(s)

print(Solution().LeftRotateString('abcdef', 3))

思路

第一反应

return s[n:] + s[:n]。

这道题考的核心是应聘者是不是可以灵活利用字符串翻转。假设字符串abcdef,n=3,设X=abc,Y=def,所以字符串可以表示成XY,如题干,问如何求得YX。假设X的翻转为XT,XT=cba,同理YT=fed,那么YX=(XTYT)T,三次翻转后可得结果。

孩子们的游戏(圆圈中最后剩下的数)🍧

题目描述:

每年六一儿童节,牛客都会准备一些小礼物和小游戏去看望孤儿院的孩子们。其中,有个游戏是这样的:首先,让 n 个小朋友们围成一个大圈,小朋友们的编号是0~n-1。然后,随机指定一个数 m ,让编号为0的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0... m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客礼品,请你试着想下,哪个小朋友会得到这份礼品呢?

img

  • 数据范围: \(1 \leq n \leq 5000 , 1 \leq m \leq 10000\)
  • 要求: 空间复杂度 \(O(1)\) ,时间复杂度 \(O(n)\)

输入描述:

2,3

输出描述:

1

说明:有2个小朋友编号为0,1,第一次报数报到3的是0号小朋友,0号小朋友出圈,1号小朋友得到礼物  

代码如下:

class Solution:
    def LastRemaining_Solution(self, n, m):
        # write code here
        n = list(range(n))
        if not n: return -1
        i = 0
        while len(n) != 1:
            for _ in range(m-1):
                i += 1
                i %= len(n)
            n.pop(i)
        return n

print(Solution().LastRemaining_Solution(5, 3))

思路

  • 约瑟夫环

构建乘积数组🍧

题目描述:

给定一个数组 A[0,1,...,n-1] ,请构建一个数组 B[0,1,...,n-1] ,其中 B 的元素 B[i]=A[0 x A[1] x ... x A[i-1] x A[i+1] x ... x A[n-1](除 A[i] 以外的全部元素的的乘积)。程序中不能使用除法。(注意:规定 B[0] = A[1] x A[2] x ...x A[n-1],B[n-1] = A[0] x A[1] x ... x A[n-2]),对于 A 长度为 1 的情况,B 无意义,故而无法构建,用例中不包括这种情况。

  • 数据范围: \(1 \leq n \leq 10\) ,数组中元㶻满足 \(|\mathrm{val}| \leq 10\)

输入描述:

[1,2,3,4,5]

输出描述:

[120,60,40,30,24]

代码如下:

class Solution:
    def multiply(self, A):
        # write code here
        B = [1] * len(A)
        temp = 1
        for i in range(1, len(A)):
            temp *= A[i-1]
            B[i] *= temp
        temp = 1
        for i in range(len(A)-2, -1, -1):
            temp *= A[i+1]
            B[i] *= temp
        return B

# [0, 1, 1]
# [1, 0, 1]
# [1, 1, 0]

思路

先算下三角,再算上三角。O(n)复杂度。

剑指 offer 题解

1. 前言

本文的绘图可通过以下途径免费获得并使用:

2. 实现 Singleton

单例模式

3. 数组中重复的数字

NowCoder

题目描述

在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

Input:
{2, 3, 1, 0, 2, 5}

Output:
2

解题思路

要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。

这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上。

以 (2, 3, 1, 0, 2, 5) 为例:

position-0 : (2,3,1,0,2,5) // 2 <-> 1
             (1,3,2,0,2,5) // 1 <-> 3
             (3,1,2,0,2,5) // 3 <-> 0
             (0,1,2,3,2,5) // already in position
position-1 : (0,1,2,3,2,5) // already in position
position-2 : (0,1,2,3,2,5) // already in position
position-3 : (0,1,2,3,2,5) // already in position
position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit

遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复。

public boolean duplicate(int[] nums, int length, int[] duplication) {
    if (nums == null || length <= 0)
        return false;
    for (int i = 0; i < length; i++) {
        while (nums[i] != i) {
            if (nums[i] == nums[nums[i]]) {
                duplication[0] = nums[i];
                return true;
            }
            swap(nums, i, nums[i]);
        }
    }
    return false;
}

private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

4. 二维数组中的查找

NowCoder

题目描述

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

Consider the following matrix:
[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

Given target = 5, return true.
Given target = 20, return false.

解题思路

从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。

复杂度:O(M + N) + O(1)

当前元素的查找区间为左下角的所有元素,例如元素 12 的查找区间如下:


public boolean Find(int target, int[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
        return false;
    int rows = matrix.length, cols = matrix[0].length;
    int r = 0, c = cols - 1; // 从右上角开始
    while (r <= rows - 1 && c >= 0) {
        if (target == matrix[r][c])
            return true;
        else if (target > matrix[r][c])
            r++;
        else
            c--;
    }
    return false;
}

5. 替换空格

NowCoder

题目描述

将一个字符串中的空格替换成 "%20"。

Input:
"We Are Happy"

Output:
"We%20Are%20Happy"

解题思路

在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。

令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。

从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。

public String replaceSpace(StringBuffer str) {
    int P1 = str.length() - 1;
    for (int i = 0; i <= P1; i++)
        if (str.charAt(i) == ' ')
            str.append("  ");

    int P2 = str.length() - 1;
    while (P1 >= 0 && P2 > P1) {
        char c = str.charAt(P1--);
        if (c == ' ') {
            str.setCharAt(P2--, '0');
            str.setCharAt(P2--, '2');
            str.setCharAt(P2--, '%');
        } else {
            str.setCharAt(P2--, c);
        }
    }
    return str.toString();
}

6. 从尾到头打印链表

NowCoder

题目描述

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


解题思路

使用栈

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    Stack<Integer> stack = new Stack<>();
    while (listNode != null) {
        stack.add(listNode.val);
        listNode = listNode.next;
    }
    ArrayList<Integer> ret = new ArrayList<>();
    while (!stack.isEmpty())
        ret.add(stack.pop());
    return ret;
}

使用递归

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (listNode != null) {
        ret.addAll(printListFromTailToHead(listNode.next));
        ret.add(listNode.val);
    }
    return ret;
}

使用头插法

利用链表头插法为逆序的特点。

头结点和第一个节点的区别:

  • 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
  • 第一个节点就是链表的第一个真正存储值的节点。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    // 头插法构建逆序链表
    ListNode head = new ListNode(-1);
    while (listNode != null) {
        ListNode memo = listNode.next;
        listNode.next = head.next;
        head.next = listNode;
        listNode = memo;
    }
    // 构建 ArrayList
    ArrayList<Integer> ret = new ArrayList<>();
    head = head.next;
    while (head != null) {
        ret.add(head.val);
        head = head.next;
    }
    return ret;
}

使用 Collections.reverse()

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> ret = new ArrayList<>();
    while (listNode != null) {
        ret.add(listNode.val);
        listNode = listNode.next;
    }
    Collections.reverse(ret);
    return ret;
}

7. 重建二叉树

NowCoder

题目描述

根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

preorder = [3,9,20,15,7]
inorder =  [9,3,15,20,7]

解题思路

前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。

// 缓存中序遍历数组每个值对应的索引
private Map<Integer, Integer> indexForInOrders = new HashMap<>();

public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
    for (int i = 0; i < in.length; i++)
        indexForInOrders.put(in[i], i);
    return reConstructBinaryTree(pre, 0, pre.length - 1, 0);
}

private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) {
    if (preL > preR)
        return null;
    TreeNode root = new TreeNode(pre[preL]);
    int inIndex = indexForInOrders.get(root.val);
    int leftTreeSize = inIndex - inL;
    root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL);
    root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
    return root;
}

8. 二叉树的下一个结点

NowCoder

题目描述

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

public class TreeLinkNode {

    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

解题思路

① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;


② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。


public TreeLinkNode GetNext(TreeLinkNode pNode) {
    if (pNode.right != null) {
        TreeLinkNode node = pNode.right;
        while (node.left != null)
            node = node.left;
        return node;
    } else {
        while (pNode.next != null) {
            TreeLinkNode parent = pNode.next;
            if (parent.left == pNode)
                return parent;
            pNode = pNode.next;
        }
    }
    return null;
}

9. 用两个栈实现队列

NowCoder

题目描述

用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。

解题思路

in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。


Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();

public void push(int node) {
    in.push(node);
}

public int pop() throws Exception {
    if (out.isEmpty())
        while (!in.isEmpty())
            out.push(in.pop());

    if (out.isEmpty())
        throw new Exception("queue is empty");

    return out.pop();
}

10.1 斐波那契数列

NowCoder

题目描述

求斐波那契数列的第 n 项,n <= 39。


解题思路

如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。


递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。

public int Fibonacci(int n) {
    if (n <= 1)
        return n;
    int[] fib = new int[n + 1];
    fib[1] = 1;
    for (int i = 2; i <= n; i++)
        fib[i] = fib[i - 1] + fib[i - 2];
    return fib[n];
}

考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。

public int Fibonacci(int n) {
    if (n <= 1)
        return n;
    int pre2 = 0, pre1 = 1;
    int fib = 0;
    for (int i = 2; i <= n; i++) {
        fib = pre2 + pre1;
        pre2 = pre1;
        pre1 = fib;
    }
    return fib;
}

由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。

public class Solution {

    private int[] fib = new int[40];

    public Solution() {
        fib[1] = 1;
        fib[2] = 2;
        for (int i = 2; i < fib.length; i++)
            fib[i] = fib[i - 1] + fib[i - 2];
    }

    public int Fibonacci(int n) {
        return fib[n];
    }
}

10.2 跳台阶

NowCoder

题目描述

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

解题思路

public int JumpFloor(int n) {
    if (n <= 2)
        return n;
    int pre2 = 1, pre1 = 2;
    int result = 1;
    for (int i = 2; i < n; i++) {
        result = pre2 + pre1;
        pre2 = pre1;
        pre1 = result;
    }
    return result;
}

10.3 矩形覆盖

NowCoder

题目描述

我们可以用 2*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2*1 的小矩形无重叠地覆盖一个 2*n 的大矩形,总共有多少种方法?

解题思路

public int RectCover(int n) {
    if (n <= 2)
        return n;
    int pre2 = 1, pre1 = 2;
    int result = 0;
    for (int i = 3; i <= n; i++) {
        result = pre2 + pre1;
        pre2 = pre1;
        pre1 = result;
    }
    return result;
}

10.4 变态跳台阶

NowCoder

题目描述

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

解题思路

public int JumpFloorII(int target) {
    int[] dp = new int[target];
    Arrays.fill(dp, 1);
    for (int i = 1; i < target; i++)
        for (int j = 0; j < i; j++)
            dp[i] += dp[j];
    return dp[target - 1];
}

11. 旋转数组的最小数字

NowCoder

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。

例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。

解题思路

在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。

本题可以修改二分查找算法进行求解:

  • 当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;
  • 否则解在 [m + 1, h] 之间,令 l = m + 1。
public int minNumberInRotateArray(int[] nums) {
    if (nums.length == 0)
        return 0;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] <= nums[h])
            h = m;
        else
            l = m + 1;
    }
    return nums[l];
}

如果数组元素允许重复的话,那么就会出现一个特殊的情况:nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。

public int minNumberInRotateArray(int[] nums) {
    if (nums.length == 0)
        return 0;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[l] == nums[m] && nums[m] == nums[h])
            return minNumber(nums, l, h);
        else if (nums[m] <= nums[h])
            h = m;
        else
            l = m + 1;
    }
    return nums[l];
}

private int minNumber(int[] nums, int l, int h) {
    for (int i = l; i < h; i++)
        if (nums[i] > nums[i + 1])
            return nums[i + 1];
    return nums[l];
}

12. 矩阵中的路径

NowCoder

题目描述

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。

例如下面的矩阵包含了一条 bfce 路径。


解题思路

private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int rows;
private int cols;

public boolean hasPath(char[] array, int rows, int cols, char[] str) {
    if (rows == 0 || cols == 0)
        return false;
    this.rows = rows;
    this.cols = cols;
    boolean[][] marked = new boolean[rows][cols];
    char[][] matrix = buildMatrix(array);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            if (backtracking(matrix, str, marked, 0, i, j))
                return true;
    return false;
}

private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, int pathLen, int r, int c) {
    if (pathLen == str.length)
        return true;
    if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c])
        return false;
    marked[r][c] = true;
    for (int[] n : next)
        if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
            return true;
    marked[r][c] = false;
    return false;
}

private char[][] buildMatrix(char[] array) {
    char[][] matrix = new char[rows][cols];
    for (int i = 0, idx = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            matrix[i][j] = array[idx++];
    return matrix;
}

13. 机器人的运动范围

NowCoder

题目描述

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

例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,37),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?

解题思路

private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int cnt = 0;
private int rows;
private int cols;
private int threshold;
private int[][] digitSum;

public int movingCount(int threshold, int rows, int cols) {
    this.rows = rows;
    this.cols = cols;
    this.threshold = threshold;
    initDigitSum();
    boolean[][] marked = new boolean[rows][cols];
    dfs(marked, 0, 0);
    return cnt;
}

private void dfs(boolean[][] marked, int r, int c) {
    if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
        return;
    marked[r][c] = true;
    if (this.digitSum[r][c] > this.threshold)
        return;
    cnt++;
    for (int[] n : next)
        dfs(marked, r + n[0], c + n[1]);
}

private void initDigitSum() {
    int[] digitSumOne = new int[Math.max(rows, cols)];
    for (int i = 0; i < digitSumOne.length; i++) {
        int n = i;
        while (n > 0) {
            digitSumOne[i] += n % 10;
            n /= 10;
        }
    }
    this.digitSum = new int[rows][cols];
    for (int i = 0; i < this.rows; i++)
        for (int j = 0; j < this.cols; j++)
            this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
}

14. 剪绳子

Leetcode

题目描述

把一根绳子剪成多段,并且使得每段的长度乘积最大。

n = 2
return 1 (2 = 1 + 1)

n = 10
return 36 (10 = 3 + 3 + 4)

解题思路

贪心

尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。

证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。

public int integerBreak(int n) {
    if (n < 2)
        return 0;
    if (n == 2)
        return 1;
    if (n == 3)
        return 2;
    int timesOf3 = n / 3;
    if (n - timesOf3 * 3 == 1)
        timesOf3--;
    int timesOf2 = (n - timesOf3 * 3) / 2;
    return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}

动态规划

public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[1] = 1;
    for (int i = 2; i <= n; i++)
        for (int j = 1; j < i; j++)
            dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j)));
    return dp[n];
}

15. 二进制中 1 的个数

NowCoder

题目描述

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

n&(n-1)

该位运算去除 n 的位级表示中最低的那一位。

n       : 10110100
n-1     : 10110011
n&(n-1) : 10110000

时间复杂度:O(M),其中 M 表示 1 的个数。

public int NumberOf1(int n) {
    int cnt = 0;
    while (n != 0) {
        cnt++;
        n &= (n - 1);
    }
    return cnt;
}

Integer.bitCount()

public int NumberOf1(int n) {
    return Integer.bitCount(n);
}

16. 数值的整数次方

NowCoder

题目描述

给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。

解题思路

下面的讨论中 x 代表 base,n 代表 exponent。


因为 (x*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。

public double Power(double base, int exponent) {
    if (exponent == 0)
        return 1;
    if (exponent == 1)
        return base;
    boolean isNegative = false;
    if (exponent < 0) {
        exponent = -exponent;
        isNegative = true;
    }
    double pow = Power(base * base, exponent / 2);
    if (exponent % 2 != 0)
        pow = pow * base;
    return isNegative ? 1 / pow : pow;
}

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

题目描述

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

解题思路

由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。

使用回溯法得到所有的数。

public void print1ToMaxOfNDigits(int n) {
    if (n <= 0)
        return;
    char[] number = new char[n];
    print1ToMaxOfNDigits(number, 0);
}

private void print1ToMaxOfNDigits(char[] number, int digit) {
    if (digit == number.length) {
        printNumber(number);
        return;
    }
    for (int i = 0; i < 10; i++) {
        number[digit] = (char) (i + '0');
        print1ToMaxOfNDigits(number, digit + 1);
    }
}

private void printNumber(char[] number) {
    int index = 0;
    while (index < number.length && number[index] == '0')
        index++;
    while (index < number.length)
        System.out.print(number[index++]);
    System.out.println();
}

18.1 在 O(1) 时间内删除链表节点

解题思路

① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。


② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。


综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N ~ 2,因此该算法的平均时间复杂度为 O(1)。

public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
    if (head == null || tobeDelete == null)
        return null;
    if (tobeDelete.next != null) {
        // 要删除的节点不是尾节点
        ListNode next = tobeDelete.next;
        tobeDelete.val = next.val;
        tobeDelete.next = next.next;
    } else {
        ListNode cur = head;
        while (cur.next != tobeDelete)
            cur = cur.next;
        cur.next = null;
    }
    return head;
}

18.2 删除链表中重复的结点

NowCoder

题目描述


解题描述

public ListNode deleteDuplication(ListNode pHead) {
    if (pHead == null || pHead.next == null)
        return pHead;
    ListNode next = pHead.next;
    if (pHead.val == next.val) {
        while (next != null && pHead.val == next.val)
            next = next.next;
        return deleteDuplication(next);
    } else {
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
    }
}

19. 正则表达式匹配

NowCoder

题目描述

请实现一个函数用来匹配包括 '.' 和 '*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '*' 表示它前面的字符可以出现任意次(包含 0 次)。

在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab*ac*a" 匹配,但是与 "aa.a" 和 "ab*a" 均不匹配。

解题思路

应该注意到,'.' 是用来当做一个任意字符,而 '*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '*' 进行类比,从而把它当成重复前面字符一次。

public boolean match(char[] str, char[] pattern) {

    int m = str.length, n = pattern.length;
    boolean[][] dp = new boolean[m + 1][n + 1];

    dp[0][0] = true;
    for (int i = 1; i <= n; i++)
        if (pattern[i - 1] == '*')
            dp[0][i] = dp[0][i - 2];

    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
                dp[i][j] = dp[i - 1][j - 1];
            else if (pattern[j - 1] == '*')
                if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') {
                    dp[i][j] |= dp[i][j - 1]; // a* counts as single a
                    dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a
                    dp[i][j] |= dp[i][j - 2]; // a* counts as empty
                } else
                    dp[i][j] = dp[i][j - 2];   // a* only counts as empty

    return dp[m][n];
}

20. 表示数值的字符串

NowCoder

题目描述

true

"+100"
"5e2"
"-123"
"3.1416"
"-1E-16"

false

"12e"
"1a3.14"
"1.2.3"
"+-5"
"12e+4.3"

解题思路

使用正则表达式进行匹配。

[]  : 字符集合
()  : 分组
?   : 重复 0 ~ 1
+   : 重复 1 ~ n
*   : 重复 0 ~ n
.   : 任意字符
\\. : 转义后的 .
\\d : 数字
public boolean isNumeric(char[] str) {
    if (str == null || str.length == 0)
        return false;
    return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}

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

NowCoder

题目描述

需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。

解题思路

public void reOrderArray(int[] nums) {
    // 奇数个数
    int oddCnt = 0;
    for (int val : nums)
        if (val % 2 == 1)
            oddCnt++;
    int[] copy = nums.clone();
    int i = 0, j = oddCnt;
    for (int num : copy) {
        if (num % 2 == 1)
            nums[i++] = num;
        else
            nums[j++] = num;
    }
}

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

NowCoder

解题思路

设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。


public ListNode FindKthToTail(ListNode head, int k) {
    if (head == null)
        return null;
    ListNode P1 = head;
    while (P1 != null && k-- > 0)
        P1 = P1.next;
    if (k > 0)
        return null;
    ListNode P2 = head;
    while (P1 != null) {
        P1 = P1.next;
        P2 = P2.next;
    }
    return P2;
}

23. 链表中环的入口结点

NowCoder

题目描述

一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。

解题思路

使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 y6 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。

在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。


public ListNode EntryNodeOfLoop(ListNode pHead) {
    if (pHead == null || pHead.next == null)
        return null;
    ListNode slow = pHead, fast = pHead;
    do {
        fast = fast.next.next;
        slow = slow.next;
    } while (slow != fast);
    fast = pHead;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}

24. 反转链表

NowCoder

解题思路

递归

public ListNode ReverseList(ListNode head) {
    if (head == null || head.next == null)
        return head;
    ListNode next = head.next;
    head.next = null;
    ListNode newHead = ReverseList(next);
    next.next = head;
    return newHead;
}

迭代

public ListNode ReverseList(ListNode head) {
    ListNode newList = new ListNode(-1);
    while (head != null) {
        ListNode next = head.next;
        head.next = newList.next;
        newList.next = head;
        head = next;
    }
    return newList.next;
}

25. 合并两个排序的链表

NowCoder

题目描述


解题思路

递归

public ListNode Merge(ListNode list1, ListNode list2) {
    if (list1 == null)
        return list2;
    if (list2 == null)
        return list1;
    if (list1.val <= list2.val) {
        list1.next = Merge(list1.next, list2);
        return list1;
    } else {
        list2.next = Merge(list1, list2.next);
        return list2;
    }
}

迭代

public ListNode Merge(ListNode list1, ListNode list2) {
    ListNode head = new ListNode(-1);
    ListNode cur = head;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            cur.next = list1;
            list1 = list1.next;
        } else {
            cur.next = list2;
            list2 = list2.next;
        }
        cur = cur.next;
    }
    if (list1 != null)
        cur.next = list1;
    if (list2 != null)
        cur.next = list2;
    return head.next;
}

26. 树的子结构

NowCoder

题目描述


解题思路

public boolean HasSubtree(TreeNode root1, TreeNode root2) {
    if (root1 == null || root2 == null)
        return false;
    return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}

private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) {
    if (root2 == null)
        return true;
    if (root1 == null)
        return false;
    if (root1.val != root2.val)
        return false;
    return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right);
}

27. 二叉树的镜像

NowCoder

题目描述


解题思路

public void Mirror(TreeNode root) {
    if (root == null)
        return;
    swap(root);
    Mirror(root.left);
    Mirror(root.right);
}

private void swap(TreeNode root) {
    TreeNode t = root.left;
    root.left = root.right;
    root.right = t;
}

28 对称的二叉树

NowCder

题目描述


解题思路

boolean isSymmetrical(TreeNode pRoot) {
    if (pRoot == null)
        return true;
    return isSymmetrical(pRoot.left, pRoot.right);
}

boolean isSymmetrical(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null)
        return true;
    if (t1 == null || t2 == null)
        return false;
    if (t1.val != t2.val)
        return false;
    return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
}

29. 顺时针打印矩阵

NowCoder

题目描述

下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10


解题思路

public ArrayList<Integer> printMatrix(int[][] matrix) {
    ArrayList<Integer> ret = new ArrayList<>();
    int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
    while (r1 <= r2 && c1 <= c2) {
        for (int i = c1; i <= c2; i++)
            ret.add(matrix[r1][i]);
        for (int i = r1 + 1; i <= r2; i++)
            ret.add(matrix[i][c2]);
        if (r1 != r2)
            for (int i = c2 - 1; i >= c1; i--)
                ret.add(matrix[r2][i]);
        if (c1 != c2)
            for (int i = r2 - 1; i > r1; i--)
                ret.add(matrix[i][c1]);
        r1++; r2--; c1++; c2--;
    }
    return ret;
}

30. 包含 min 函数的栈

NowCoder

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。

解题思路

private Stack<Integer> dataStack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();

public void push(int node) {
    dataStack.push(node);
    minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}

public void pop() {
    dataStack.pop();
    minStack.pop();
}

public int top() {
    return dataStack.peek();
}

public int min() {
    return minStack.peek();
}

31. 栈的压入、弹出序列

NowCoder

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。

例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。

解题思路

使用一个栈来模拟压入弹出操作。

public boolean IsPopOrder(int[] pushSequence, int[] popSequence) {
    int n = pushSequence.length;
    Stack<Integer> stack = new Stack<>();
    for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
        stack.push(pushSequence[pushIndex]);
        while (popIndex < n && !stack.isEmpty() 
                && stack.peek() == popSequence[popIndex]) {
            stack.pop();
            popIndex++;
        }
    }
    return stack.isEmpty();
}

32.1 从上往下打印二叉树

NowCoder

题目描述

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7


解题思路

使用队列来进行层次遍历。

不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。

public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
    Queue<TreeNode> queue = new LinkedList<>();
    ArrayList<Integer> ret = new ArrayList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode t = queue.poll();
            if (t == null)
                continue;
            ret.add(t.val);
            queue.add(t.left);
            queue.add(t.right);
        }
    }
    return ret;
}

32.2 把二叉树打印成多行

NowCoder

题目描述

和上题几乎一样。

解题思路

ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(pRoot);
    while (!queue.isEmpty()) {
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode node = queue.poll();
            if (node == null)
                continue;
            list.add(node.val);
            queue.add(node.left);
            queue.add(node.right);
        }
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

32.3 按之字形顺序打印二叉树

NowCoder

题目描述

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

解题思路

public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(pRoot);
    boolean reverse = false;
    while (!queue.isEmpty()) {
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode node = queue.poll();
            if (node == null)
                continue;
            list.add(node.val);
            queue.add(node.left);
            queue.add(node.right);
        }
        if (reverse)
            Collections.reverse(list);
        reverse = !reverse;
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

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

NowCoder

题目描述

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。

例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。


解题思路

public boolean VerifySquenceOfBST(int[] sequence) {
    if (sequence == null || sequence.length == 0)
        return false;
    return verify(sequence, 0, sequence.length - 1);
}

private boolean verify(int[] sequence, int first, int last) {
    if (last - first <= 1)
        return true;
    int rootVal = sequence[last];
    int cutIndex = first;
    while (cutIndex < last && sequence[cutIndex] <= rootVal)
        cutIndex++;
    for (int i = cutIndex; i < last; i++)
        if (sequence[i] < rootVal)
            return false;
    return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
}

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

NowCoder

题目描述

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

下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12


解题思路

private ArrayList<ArrayList<Integer>> ret = new ArrayList<>();

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
    backtracking(root, target, new ArrayList<>());
    return ret;
}

private void backtracking(TreeNode node, int target, ArrayList<Integer> path) {
    if (node == null)
        return;
    path.add(node.val);
    target -= node.val;
    if (target == 0 && node.left == null && node.right == null) {
        ret.add(new ArrayList<>(path));
    } else {
        backtracking(node.left, target, path);
        backtracking(node.right, target, path);
    }
    path.remove(path.size() - 1);
}

35. 复杂链表的复制

NowCoder

题目描述

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。

public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}

解题思路

第一步,在每个节点的后面插入复制的节点。


第二步,对复制节点的 random 链接进行赋值。


第三步,拆分。


public RandomListNode Clone(RandomListNode pHead) {
    if (pHead == null)
        return null;
    // 插入新节点
    RandomListNode cur = pHead;
    while (cur != null) {
        RandomListNode clone = new RandomListNode(cur.label);
        clone.next = cur.next;
        cur.next = clone;
        cur = clone.next;
    }
    // 建立 random 链接
    cur = pHead;
    while (cur != null) {
        RandomListNode clone = cur.next;
        if (cur.random != null)
            clone.random = cur.random.next;
        cur = clone.next;
    }
    // 拆分
    cur = pHead;
    RandomListNode pCloneHead = pHead.next;
    while (cur.next != null) {
        RandomListNode next = cur.next;
        cur.next = next.next;
        cur = next;
    }
    return pCloneHead;
}

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

NowCoder

题目描述

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


解题思路

private TreeNode pre = null;
private TreeNode head = null;

public TreeNode Convert(TreeNode root) {
    inOrder(root);
    return head;
}

private void inOrder(TreeNode node) {
    if (node == null)
        return;
    inOrder(node.left);
    node.left = pre;
    if (pre != null)
        pre.right = node;
    pre = node;
    if (head == null)
        head = node;
    inOrder(node.right);
}

37. 序列化二叉树

NowCoder

题目描述

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

解题思路

private String deserializeStr;

public String Serialize(TreeNode root) {
    if (root == null)
        return "#";
    return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
}

public TreeNode Deserialize(String str) {
    deserializeStr = str;
    return Deserialize();
}

private TreeNode Deserialize() {
    if (deserializeStr.length() == 0)
        return null;
    int index = deserializeStr.indexOf(" ");
    String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
    deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
    if (node.equals("#"))
        return null;
    int val = Integer.valueOf(node);
    TreeNode t = new TreeNode(val);
    t.left = Deserialize();
    t.right = Deserialize();
    return t;
}

38. 字符串的排列

NowCoder

题目描述

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

解题思路

private ArrayList<String> ret = new ArrayList<>();

public ArrayList<String> Permutation(String str) {
    if (str.length() == 0)
        return ret;
    char[] chars = str.toCharArray();
    Arrays.sort(chars);
    backtracking(chars, new boolean[chars.length], new StringBuilder());
    return ret;
}

private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) {
    if (s.length() == chars.length) {
        ret.add(s.toString());
        return;
    }
    for (int i = 0; i < chars.length; i++) {
        if (hasUsed[i])
            continue;
        if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */
            continue;
        hasUsed[i] = true;
        s.append(chars[i]);
        backtracking(chars, hasUsed, s);
        s.deleteCharAt(s.length() - 1);
        hasUsed[i] = false;
    }
}

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

NowCoder

解题思路

多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。

使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。

public int MoreThanHalfNum_Solution(int[] nums) {
    int majority = nums[0];
    for (int i = 1, cnt = 1; i < nums.length; i++) {
        cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
        if (cnt == 0) {
            majority = nums[i];
            cnt = 1;
        }
    }
    int cnt = 0;
    for (int val : nums)
        if (val == majority)
            cnt++;
    return cnt > nums.length / 2 ? majority : 0;
}

40. 最小的 K 个数

NowCoder

解题思路

快速选择

  • 复杂度:O(N) + O(1)
  • 只有当允许修改数组元素时才可以使用

快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。

public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (k > nums.length || k <= 0)
        return ret;
    findKthSmallest(nums, k - 1);
    /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
    for (int i = 0; i < k; i++)
        ret.add(nums[i]);
    return ret;
}

public void findKthSmallest(int[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int j = partition(nums, l, h);
        if (j == k)
            break;
        if (j > k)
            h = j - 1;
        else
            l = j + 1;
    }
}

private int partition(int[] nums, int l, int h) {
    int p = nums[l];     /* 切分元素 */
    int i = l, j = h + 1;
    while (true) {
        while (i != h && nums[++i] < p) ;
        while (j != l && nums[--j] > p) ;
        if (i >= j)
            break;
        swap(nums, i, j);
    }
    swap(nums, l, j);
    return j;
}

private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

大小为 K 的最小堆

  • 复杂度:O(NlogK) + O(K)
  • 特别适合处理海量数据

应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。

维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。

public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
    if (k > nums.length || k <= 0)
        return new ArrayList<>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
    for (int num : nums) {
        maxHeap.add(num);
        if (maxHeap.size() > k)
            maxHeap.poll();
    }
    return new ArrayList<>(maxHeap);
}

41.1 数据流中的中位数

NowCoder

题目描述

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

解题思路

/* 大顶堆,存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;

public void Insert(Integer val) {
    /* 插入要保证两个堆存于平衡状态 */
    if (N % 2 == 0) {
        /* N 为偶数的情况下插入到右半边。
         * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
         * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
        left.add(val);
        right.add(left.poll());
    } else {
        right.add(val);
        left.add(right.poll());
    }
    N++;
}

public Double GetMedian() {
    if (N % 2 == 0)
        return (left.peek() + right.peek()) / 2.0;
    else
        return (double) right.peek();
}

41.2 字符流中第一个不重复的字符

NowCoder

题目描述

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。

解题思路

private int[] cnts = new int[256];
private Queue<Character> queue = new LinkedList<>();

public void Insert(char ch) {
    cnts[ch]++;
    queue.add(ch);
    while (!queue.isEmpty() && cnts[queue.peek()] > 1)
        queue.poll();
}

public char FirstAppearingOnce() {
    return queue.isEmpty() ? '#' : queue.peek();
}

42. 连续子数组的最大和

NowCoder

题目描述

{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。

解题思路

public int FindGreatestSumOfSubArray(int[] nums) {
    if (nums == null || nums.length == 0)
        return 0;
    int greatestSum = Integer.MIN_VALUE;
    int sum = 0;
    for (int val : nums) {
        sum = sum <= 0 ? val : sum + val;
        greatestSum = Math.max(greatestSum, sum);
    }
    return greatestSum;
}

43. 从 1 到 n 整数中 1 出现的次数

NowCoder

解题思路

public int NumberOf1Between1AndN_Solution(int n) {
    int cnt = 0;
    for (int m = 1; m <= n; m *= 10) {
        int a = n / m, b = n % m;
        cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
    }
    return cnt;
}

Leetcode : 233. Number of Digit One

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

题目描述

数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。

解题思路

public int getDigitAtIndex(int index) {
    if (index < 0)
        return -1;
    int place = 1;  // 1 表示个位,2 表示 十位...
    while (true) {
        int amount = getAmountOfPlace(place);
        int totalAmount = amount * place;
        if (index < totalAmount)
            return getDigitAtIndex(index, place);
        index -= totalAmount;
        place++;
    }
}

/**
 * place 位数的数字组成的字符串长度
 * 10, 90, 900, ...
 */
private int getAmountOfPlace(int place) {
    if (place == 1)
        return 10;
    return (int) Math.pow(10, place - 1) * 9;
}

/**
 * place 位数的起始数字
 * 0, 10, 100, ...
 */
private int getBeginNumberOfPlace(int place) {
    if (place == 1)
        return 0;
    return (int) Math.pow(10, place - 1);
}

/**
 * 在 place 位数组成的字符串中,第 index 个数
 */
private int getDigitAtIndex(int index, int place) {
    int beginNumber = getBeginNumberOfPlace(place);
    int shiftNumber = index / place;
    String number = (beginNumber + shiftNumber) + "";
    int count = index % place;
    return number.charAt(count) - '0';
}

45. 把数组排成最小的数

NowCoder

题目描述

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

解题思路

可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。

public String PrintMinNumber(int[] numbers) {
    if (numbers == null || numbers.length == 0)
        return "";
    int n = numbers.length;
    String[] nums = new String[n];
    for (int i = 0; i < n; i++)
        nums[i] = numbers[i] + "";
    Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));
    String ret = "";
    for (String str : nums)
        ret += str;
    return ret;
}

46. 把数字翻译成字符串

Leetcode

题目描述

给定一个数字,按照如下规则翻译成字符串:0 翻译成“a”,1 翻译成“b”... 25 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。

解题思路

public int numDecodings(String s) {
    if (s == null || s.length() == 0)
        return 0;
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = s.charAt(0) == '0' ? 0 : 1;
    for (int i = 2; i <= n; i++) {
        int one = Integer.valueOf(s.substring(i - 1, i));
        if (one != 0)
            dp[i] += dp[i - 1];
        if (s.charAt(i - 2) == '0')
            continue;
        int two = Integer.valueOf(s.substring(i - 2, i));
        if (two <= 26)
            dp[i] += dp[i - 2];
    }
    return dp[n];
}

47. 礼物的最大价值

NowCoder

题目描述

在一个 m*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘

1    10   3    8
12   2    9    6
5    7    4    11
3    7    16   5

礼物的最大价值为 1+12+5+7+7+16+5=53。

解题思路

应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。

public int getMost(int[][] values) {
    if (values == null || values.length == 0 || values[0].length == 0)
        return 0;
    int n = values[0].length;
    int[] dp = new int[n];
    for (int[] value : values) {
        dp[0] += value[0];
        for (int i = 1; i < n; i++)
            dp[i] = Math.max(dp[i], dp[i - 1]) + value[i];
    }
    return dp[n - 1];
}

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

题目描述

输入一个字符串(只包含 a~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。

解题思路

public int longestSubStringWithoutDuplication(String str) {
    int curLen = 0;
    int maxLen = 0;
    int[] preIndexs = new int[26];
    Arrays.fill(preIndexs, -1);
    for (int curI = 0; curI < str.length(); curI++) {
        int c = str.charAt(curI) - 'a';
        int preI = preIndexs[c];
        if (preI == -1 || curI - preI > curLen) {
            curLen++;
        } else {
            maxLen = Math.max(maxLen, curLen);
            curLen = curI - preI;
        }
        preIndexs[c] = curI;
    }
    maxLen = Math.max(maxLen, curLen);
    return maxLen;
}

49. 丑数

NowCoder

题目描述

把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。

解题思路

public int GetUglyNumber_Solution(int N) {
    if (N <= 6)
        return N;
    int i2 = 0, i3 = 0, i5 = 0;
    int[] dp = new int[N];
    dp[0] = 1;
    for (int i = 1; i < N; i++) {
        int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5;
        dp[i] = Math.min(next2, Math.min(next3, next5));
        if (dp[i] == next2)
            i2++;
        if (dp[i] == next3)
            i3++;
        if (dp[i] == next5)
            i5++;
    }
    return dp[N - 1];
}

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

NowCoder

题目描述

在一个字符串中找到第一个只出现一次的字符,并返回它的位置。

解题思路

最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。

public int FirstNotRepeatingChar(String str) {
    int[] cnts = new int[256];
    for (int i = 0; i < str.length(); i++)
        cnts[str.charAt(i)]++;
    for (int i = 0; i < str.length(); i++)
        if (cnts[str.charAt(i)] == 1)
            return i;
    return -1;
}

以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。

public int FirstNotRepeatingChar2(String str) {
    BitSet bs1 = new BitSet(256);
    BitSet bs2 = new BitSet(256);
    for (char c : str.toCharArray()) {
        if (!bs1.get(c) && !bs2.get(c))
            bs1.set(c);     // 0 0 -> 0 1
        else if (bs1.get(c) && !bs2.get(c))
            bs2.set(c);     // 0 1 -> 1 1
    }
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (bs1.get(c) && !bs2.get(c))  // 0 1
            return i;
    }
    return -1;
}

51. 数组中的逆序对

NowCoder

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

解题思路

private long cnt = 0;
private int[] tmp;  // 在这里声明辅助数组,而不是在 merge() 递归函数中声明

public int InversePairs(int[] nums) {
    tmp = new int[nums.length];
    mergeSort(nums, 0, nums.length - 1);
    return (int) (cnt % 1000000007);
}

private void mergeSort(int[] nums, int l, int h) {
    if (h - l < 1)
        return;
    int m = l + (h - l) / 2;
    mergeSort(nums, l, m);
    mergeSort(nums, m + 1, h);
    merge(nums, l, m, h);
}

private void merge(int[] nums, int l, int m, int h) {
    int i = l, j = m + 1, k = l;
    while (i <= m || j <= h) {
        if (i > m)
            tmp[k] = nums[j++];
        else if (j > h)
            tmp[k] = nums[i++];
        else if (nums[i] < nums[j])
            tmp[k] = nums[i++];
        else {
            tmp[k] = nums[j++];
            this.cnt += m - i + 1;  // nums[i] >= nums[j],说明 nums[i...mid] 都大于 nums[j]
        }
        k++;
    }
    for (k = l; k <= h; k++)
        nums[k] = tmp[k];
}

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

NowCoder

题目描述


解题思路

设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。

当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    ListNode l1 = pHead1, l2 = pHead2;
    while (l1 != l2) {
        l1 = (l1 == null) ? pHead2 : l1.next;
        l2 = (l2 == null) ? pHead1 : l2.next;
    }
    return l1;
}

53. 数字在排序数组中出现的次数

NowCoder

题目描述

Input:
nums = 1, 2, 3, 3, 3, 3, 4, 6
K = 3

Output:
4

解题思路

public int GetNumberOfK(int[] nums, int K) {
    int first = binarySearch(nums, K);
    int last = binarySearch(nums, K + 1);
    return (first == nums.length || nums[first] != K) ? 0 : last - first;
}

private int binarySearch(int[] nums, int K) {
    int l = 0, h = nums.length;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] >= K)
            h = m;
        else
            l = m + 1;
    }
    return l;
}

54. 二叉查找树的第 K 个结点

NowCoder

解题思路

利用二叉查找树中序遍历有序的特点。

private TreeNode ret;
private int cnt = 0;

public TreeNode KthNode(TreeNode pRoot, int k) {
    inOrder(pRoot, k);
    return ret;
}

private void inOrder(TreeNode root, int k) {
    if (root == null || cnt >= k)
        return;
    inOrder(root.left, k);
    cnt++;
    if (cnt == k)
        ret = root;
    inOrder(root.right, k);
}

55.1 二叉树的深度

NowCoder

题目描述

从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。


解题思路

public int TreeDepth(TreeNode root) {
    return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right));
}

55.2 平衡二叉树

NowCoder

题目描述

平衡二叉树左右子树高度差不超过 1。


解题思路

private boolean isBalanced = true;

public boolean IsBalanced_Solution(TreeNode root) {
    height(root);
    return isBalanced;
}

private int height(TreeNode root) {
    if (root == null || !isBalanced)
        return 0;
    int left = height(root.left);
    int right = height(root.right);
    if (Math.abs(left - right) > 1)
        isBalanced = false;
    return 1 + Math.max(left, right);
}

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

NowCoder

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。

解题思路

两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。

diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。

public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) {
    int diff = 0;
    for (int num : nums)
        diff ^= num;
    diff &= -diff;
    for (int num : nums) {
        if ((num & diff) == 0)
            num1[0] ^= num;
        else
            num2[0] ^= num;
    }
}

57.1 和为 S 的两个数字

NowCoder

题目描述

输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。

解题思路

使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

  • 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
  • 如果 sum > target,移动较大的元素,使 sum 变小一些;
  • 如果 sum < target,移动较小的元素,使 sum 变大一些。
public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
    int i = 0, j = array.length - 1;
    while (i < j) {
        int cur = array[i] + array[j];
        if (cur == sum)
            return new ArrayList<>(Arrays.asList(array[i], array[j]));
        if (cur < sum)
            i++;
        else
            j--;
    }
    return new ArrayList<>();
}

57.2 和为 S 的连续正数序列

NowCoder

题目描述

输出所有和为 S 的连续正数序列。

例如和为 100 的连续序列有:

[9, 10, 11, 12, 13, 14, 15, 16]
[18, 19, 20, 21, 22]。

解题思路

public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
    ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
    int start = 1, end = 2;
    int curSum = 3;
    while (end < sum) {
        if (curSum > sum) {
            curSum -= start;
            start++;
        } else if (curSum < sum) {
            end++;
            curSum += end;
        } else {
            ArrayList<Integer> list = new ArrayList<>();
            for (int i = start; i <= end; i++)
                list.add(i);
            ret.add(list);
            curSum -= start;
            start++;
            end++;
            curSum += end;
        }
    }
    return ret;
}

58.1 翻转单词顺序列

NowCoder

题目描述

Input:
"I am a student."

Output:
"student. a am I"

解题思路

题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(N),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。

正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。

public String ReverseSentence(String str) {
    int n = str.length();
    char[] chars = str.toCharArray();
    int i = 0, j = 0;
    while (j <= n) {
        if (j == n || chars[j] == ' ') {
            reverse(chars, i, j - 1);
            i = j + 1;
        }
        j++;
    }
    reverse(chars, 0, n - 1);
    return new String(chars);
}

private void reverse(char[] c, int i, int j) {
    while (i < j)
        swap(c, i++, j--);
}

private void swap(char[] c, int i, int j) {
    char t = c[i];
    c[i] = c[j];
    c[j] = t;
}

58.2 左旋转字符串

NowCoder

题目描述

Input:
S="abcXYZdef"
K=3

Output:
"XYZdefabc"

解题思路

先将 "abc" 和 "XYZdef" 分别翻转,得到 "cbafedZYX",然后再把整个字符串翻转得到 "XYZdefabc"。

public String LeftRotateString(String str, int n) {
    if (n >= str.length())
        return str;
    char[] chars = str.toCharArray();
    reverse(chars, 0, n - 1);
    reverse(chars, n, chars.length - 1);
    reverse(chars, 0, chars.length - 1);
    return new String(chars);
}

private void reverse(char[] chars, int i, int j) {
    while (i < j)
        swap(chars, i++, j--);
}

private void swap(char[] chars, int i, int j) {
    char t = chars[i];
    chars[i] = chars[j];
    chars[j] = t;
}

59. 滑动窗口的最大值

NowCoder

题目描述

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。

例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。

解题思路

public ArrayList<Integer> maxInWindows(int[] num, int size) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (size > num.length || size < 1)
        return ret;
    PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);  /* 大顶堆 */
    for (int i = 0; i < size; i++)
        heap.add(num[i]);
    ret.add(heap.peek());
    for (int i = 1, j = i + size - 1; j < num.length; i++, j++) {            /* 维护一个大小为 size 的大顶堆 */
        heap.remove(num[i - 1]);
        heap.add(num[j]);
        ret.add(heap.peek());
    }
    return ret;
}

60. n 个骰子的点数

Lintcode

题目描述

把 n 个骰子仍在地上,求点数和为 s 的概率。

解题思路

动态规划解法

使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。

空间复杂度:O(N2)

public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[n + 1][pointNum + 1];

    for (int i = 1; i <= face; i++)
        dp[1][i] = 1;

    for (int i = 2; i <= n; i++)
        for (int j = i; j <= pointNum; j++)     /* 使用 i 个骰子最小点数为 i */
            for (int k = 1; k <= face && k <= j; k++)
                dp[i][j] += dp[i - 1][j - k];

    final double totalNum = Math.pow(6, n);
    List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
    for (int i = n; i <= pointNum; i++)
        ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum));

    return ret;
}

动态规划解法 + 旋转数组

空间复杂度:O(N)

public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[2][pointNum + 1];

    for (int i = 1; i <= face; i++)
        dp[0][i] = 1;

    int flag = 1;                                     /* 旋转标记 */
    for (int i = 2; i <= n; i++, flag = 1 - flag) {
        for (int j = 0; j <= pointNum; j++)
            dp[flag][j] = 0;                          /* 旋转数组清零 */

        for (int j = i; j <= pointNum; j++)
            for (int k = 1; k <= face && k <= j; k++)
                dp[flag][j] += dp[1 - flag][j - k];
    }

    final double totalNum = Math.pow(6, n);
    List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
    for (int i = n; i <= pointNum; i++)
        ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum));

    return ret;
}

61. 扑克牌顺子

NowCoder

题目描述

五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。

解题思路

public boolean isContinuous(int[] nums) {

    if (nums.length < 5)
        return false;

    Arrays.sort(nums);

    // 统计癞子数量
    int cnt = 0;
    for (int num : nums)
        if (num == 0)
            cnt++;

    // 使用癞子去补全不连续的顺子
    for (int i = cnt; i < nums.length - 1; i++) {
        if (nums[i + 1] == nums[i])
            return false;
        cnt -= nums[i + 1] - nums[i] - 1;
    }

    return cnt >= 0;
}

62. 圆圈中最后剩下的数

NowCoder

题目描述

让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。

解题思路

约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。

public int LastRemaining_Solution(int n, int m) {
    if (n == 0)     /* 特殊输入的处理 */
        return -1;
    if (n == 1)     /* 递归返回条件 */
        return 0;
    return (LastRemaining_Solution(n - 1, m) + m) % n;
}

63. 股票的最大利润

Leetcode

题目描述

可以有一次买入和一次卖出,那么买入必须在前。求最大收益。

解题思路

使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。

public int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0)
        return 0;
    int soFarMin = prices[0];
    int maxProfit = 0;
    for (int i = 1; i < prices.length; i++) {
        soFarMin = Math.min(soFarMin, prices[i]);
        maxProfit = Math.max(maxProfit, prices[i] - soFarMin);
    }
    return maxProfit;
}

64. 求 1+2+3+...+n

NowCoder

题目描述

要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。

解题思路

使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。

条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。

本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。

public int Sum_Solution(int n) {
    int sum = n;
    boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0);
    return sum;
}

65. 不用加减乘除做加法

NowCoder

题目描述

写一个函数,求两个整数之和,要求不得使用 +、-、*、/ 四则运算符号。

解题思路

a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。

递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。

public int Add(int a, int b) {
    return b == 0 ? a : Add(a ^ b, (a & b) << 1);
}

66. 构建乘积数组

NowCoder

题目描述

给定一个数组 A[0, 1,..., n-1],请构建一个数组 B[0, 1,..., n-1],其中 B 中的元素 B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。要求不能使用除法。

解题思路

public int[] multiply(int[] A) {
    int n = A.length;
    int[] B = new int[n];
    for (int i = 0, product = 1; i < n; product *= A[i], i++)       /* 从左往右累乘 */
        B[i] = product;
    for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--)  /* 从右往左累乘 */
        B[i] *= product;
    return B;
}

67. 把字符串转换成整数

NowCoder

题目描述

将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。

Iuput:
+2147483647
1a33

Output:
2147483647
0

解题思路

public int StrToInt(String str) {
    if (str == null || str.length() == 0)
        return 0;
    boolean isNegative = str.charAt(0) == '-';
    int ret = 0;
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (i == 0 && (c == '+' || c == '-'))  /* 符号判定 */
            continue;
        if (c < '0' || c > '9')                /* 非法输入 */
            return 0;
        ret = ret * 10 + (c - '0');
    }
    return isNegative ? -ret : ret;
}

68. 树中两个节点的最低公共祖先

解题思路

二叉查找树


Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree

二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null)
        return root;
    if (root.val > p.val && root.val > q.val)
        return lowestCommonAncestor(root.left, p, q);
    if (root.val < p.val && root.val < q.val)
        return lowestCommonAncestor(root.right, p, q);
    return root;
}

普通二叉树


Leetcode : 236. Lowest Common Ancestor of a Binary Tree

在左右子树中查找是否存在 p 或者 q,如果 p 和 q 分别在两个子树中,那么就说明根节点就是最低公共祖先。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q)
        return root;
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    return left == null ? right : right == null ? left : root;
}

参考文献

  • 何海涛. 剑指 Offer[M]. 电子工业出版社, 2012.
posted @ 2022-01-18 17:49  梁君牧  阅读(322)  评论(0编辑  收藏  举报