《剑指Offer》专题:第四期 31-40

《剑指Offer》专题:第四期 31-40

这里是《剑指Offer》专题第四期,本期内容为牛客网序第31-40题的解法与分析。涉及链表、数组、树等数据结构及其相关的算法,题目预览,见下图。
牛客网31-40.png

题目31:整数中1出现的次数

求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

# -*- coding:utf-8 -*-
"""
方法1:
    暴力求解:时间O(nlgn),空间O(1)
方法2:
    巧解法:时间O(C*n),C为最大整数的长度(对于Java,C++等语言来说,C为常数),空间O(1)
方法3:
    规律公式法:时间 O(lgn),空间O(1),最优解
    参考:https://www.nowcoder.com/questionTerminal/bd7f978302044eee894445e244c7eee6
"""
class Solution:
    def NumberOf1Between1AndN_Solution(self, n):
        
        #方法1:暴力法:
        """
        def func(num):
            cnt = 0
            while(num):
                if num%10 ==1:
                    cnt +=1
                num = num/10
            return cnt
        count = 0
        for i in range(1,n+1):
            count += func(i)
        return count
        """
        
        #方法2:巧解法,把每个整数转为字符串,统计每个字符串1的个数,再相加
        """
        if n <= 0:#依据题意
            return 0
        count = 0
        for i in range(1,n+1):
            x = str(i)
            for ch in x:
                if ch == '1':
                    count += 1
        return count
        """
        
        #方法3:规律公式求法,最优解
        count = 0
        m = 1
        while m<=n:
            #若是判断包含x(0~9)的个数,只需将(n/m%10 == 1)改成(n/m%10 == x)
            count += (n/m + 8)/10*m + (n/m%10 == 1)*(n%m + 1) 
            m *= 10
        return count
题目32:把数组排成最小的数

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

# -*- coding:utf-8 -*-
"""
方法1: 时间复杂度O(n+n-1+n-2+……+2+1)=O(n^2),空间复杂度O(1)
    要得到最小的数字,其实是要将数组中的数字按构成最小数字的方式排列,然后转换成字符串即可
    而这个方式就是 让数组中数字转成字符串拼接,取两个,若S1+S2>S2+S1则交换S1和S2的位置
方法2:
    方法2思想与方法1一样,只不过是利用python sort函数的cmp参数实现而已
"""
class Solution:
    def PrintMinNumber(self, numbers):
        # 方法1:自己实现排列规则
        if not numbers:
            return ""
        length = len(numbers)
        for i in range(length):
            for j in range(i+1,length):
                a = str(numbers[i])+str(numbers[j])
                b = str(numbers[j])+str(numbers[i])
                if a > b: #前面+后面 大于 后面+前面,则交换数字在数组中位置
                    numbers[i],numbers[j] = numbers[j],numbers[i]
        res = ""
        for num in numbers:
            res += str(num)
        return res
        
        
        #方法2:利用python内置函数简化方法1代码
        """
        def exchangeSignal(x,y):
            if x+y>y+x:
                return 1
            elif x+y<y+x:
                return -1
            else:
                return 0
        if not numbers:
            return ""
        numbers = map(str,numbers)
                #利用cmp参数实现方法1的比较交换功能
        numbers.sort(cmp = lambda x,y:exchangeSignal(x,y))
        res = ""
        for num in numbers:
            res += str(num)
        return res
        """
题目33:丑数

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

# -*- coding:utf-8 -*-
"""
根据牛客网代码改编,更加清晰和节省时间
"""
class Solution:
    def GetUglyNumber_Solution(self, index):
        # 鲁棒性代码
        if index < 7: #小于7时,index本身就是第index个丑数
            return index
        p1, p2, p3 = 0, 0, 0 #三个指针位
        uglynum_lst = [1] #初始化
        length_lst = 1  #丑数数组长度计数
        while length_lst< index:
            nextugly = min(2*uglynum_lst[p1],3*uglynum_lst[p2],5*uglynum_lst[p3])
            if uglynum_lst[p1]*2 == nextugly: p1 += 1
            if uglynum_lst[p2]*3 == nextugly: p2 += 1
            if uglynum_lst[p3]*5 == nextugly: p3 += 1
            uglynum_lst.append(nextugly)
            length_lst += 1
        return uglynum_lst[-1] # 返回表尾元素,若需返回前N个丑数,去掉【-1】即可。
"""
参考链接:https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b
来源:牛客网
通俗易懂的解释:
首先从丑数的定义我们知道,一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方法会得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:
(1)丑数数组: 1
乘以2的队列:2
乘以3的队列:3
乘以5的队列:5
选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(2)丑数数组:1,2
乘以2的队列:4
乘以3的队列:3,6
乘以5的队列:5,10
选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(3)丑数数组:1,2,3
乘以2的队列:4,6
乘以3的队列:6,9
乘以5的队列:5,10,15
选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(4)丑数数组:1,2,3,4
乘以2的队列:6,8
乘以3的队列:6,9,12
乘以5的队列:5,10,15,20
选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(5)丑数数组:1,2,3,4,5
乘以2的队列:6,8,10,
乘以3的队列:6,9,12,15
乘以5的队列:10,15,20,25
选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列;
……………………
疑问:
1.为什么分三个队列?
丑数数组里的数一定是有序的,因为我们是从丑数数组里的数乘以2,3,5选出的最小数,一定比以前未乘以2,3,5大,同时对于三个队列内部,按先后顺序乘以2,3,5分别放入,所以同一个队列内部也是有序的;
2.为什么比较三个队列头部最小的数放入丑数数组?
因为三个队列是有序的,所以取出三个头中最小的,等同于找到了三个队列所有数中最小的。
实现思路:
我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;“|”表示指针,arr表示丑数数组;
(1)1
|2
|3
|5
目前指针指向0,0,0,队列头arr[0] * 2 = 2,  arr[0] * 3 = 3,  arr[0] * 5 = 5
(2)1 2
2 |4
|3 6
|5 10
目前指针指向1,0,0,队列头arr[1] * 2 = 4,  arr[0] * 3 = 3, arr[0] * 5 = 5
(3)1 2 3
2| 4 6
3 |6 9
|5 10 15
目前指针指向1,1,0,队列头arr[1] * 2 = 4,  arr[1] * 3 = 6, arr[0] * 5 = 5
"""

####题目34:第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

# -*- coding:utf-8 -*-
"""
时间复杂度O(n)
空间复杂度O(1)
map的思想:
    用一个数组记录字符出现的位置,初始化为257,遍历字符串s
    若 该字符的对应数组中的值257,说明未加入数组,置为 i,记录位置
    若 不为257,说明重复出现;置为258
    最后,返回数组中最小的那个数,就是只出现一次字符的位置(<257),否则返回 -1
"""
class Solution:
    def FirstNotRepeatingChar(self, s):
        
        # 鲁棒性检测
        if not s:
            return -1
        
        #用一个数组记录每个字符出现的下标
        record = [257]*256 #设置256为数组长度,hash
        for i in range(len(s)):
            if record[ord(s[i])] == 257:#没出现过,保存出现的位置
                record[ord(s[i])] = i
            else: #出现过了,对应位置置258
                record[ord(s[i])] = 258
        
        #找到下标最小的那个就是第一个只出现一次的字符
        firstOnceindex = 257
        for index in record:
            if index < firstOnceindex:
                firstOnceindex = index
        
        #返回第一个只出现一次字符位置 或者找不到返回-1
        if firstOnceindex<257:
            return firstOnceindex
        else:
            return -1

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

# -*- coding:utf-8 -*-
"""
此题未能在牛客网上AC,但思路可看。
"""
count = 0
class Solution:
    def InversePairs(self, data):
        global count
        def MergeSort(lists):
            global count
            if len(lists) <= 1:
                return lists
            num = int( len(lists)/2 )
            left = MergeSort(lists[:num])
            right = MergeSort(lists[num:])
            r, l=0, 0
            result=[]
            while l<len(left) and r<len(right):
                if left[l] < right[r]:
                    result.append(left[l])
                    l += 1
                else:
                    result.append(right[r])
                    r += 1
                    count += len(left)-l
            result += right[r:]
            result += left[l:]
            return result
        MergeSort(data)
        return count%1000000007
        
        # 方法1: 两个循环,逐项比较O(n^2),不通过
        """
        if not data:
            return 0
        n = len(data)
        count = 0
        for i in range(n):
            for j in range(i+1,n):
                if data[i]>data[j]:
                    count = (count+1)
        return count%1000000007
        """

####题目36:两个链表的公共结点
输入两个链表,找出它们的第一个公共结点。

# -*- coding:utf-8 -*-
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None
"""
方法1: 快慢指针,让长的先走几步,然后再一起走,遇到相同结点,返回;时间O(m+n),空间O(1)
方法2: hash法,把表1hash了,遍历另一个表,查看是否存在表1的结点;时间O(m+n),空间O(m)
"""
class Solution:
    def FindFirstCommonNode(self, pHead1, pHead2):
        # 方法1:双指针,快慢指针
        """
        #鲁棒性检测
        if not pHead1 or not pHead2:
            return None
        
        #初始化参数,记录表头结点
        len1,len2 = 1,1
        p1 = ListNode(0)
        p2 = ListNode(0)
        p1.next = pHead1
        p2.next = pHead2
        
        #统计两个表各自长度
        while pHead1 and pHead1.next:
            pHead1 = pHead1.next
            len1 += 1
        while pHead2 and pHead2.next:
            pHead2 = pHead2.next
            len2 += 1
        
        #让长的表先走几步,然后两个表一起走,遇到相同的节点则是公共节点
        if len1>=len2:
            for i in range(len1-len2):
                p1 = p1.next
            while p1!= p2 and p1.next and p2.next:
                p1 = p1.next
                p2 = p2.next
        if len1<len2:
            for i in range(len2-len1):
                p2 = p2.next
            while p1!= p2 and p1.next and p2.next:
                p1 = p1.next
                p2 = p2.next
        if p1 == p2:
            return p1
        else:
            return None
        """
        
        #方法2:hash
        #鲁棒性检测
        if not pHead1 or not pHead2:
            return None
        hashset = set()
        while pHead1:
            hashset.add(pHead1)
            pHead1 = pHead1.next
        while pHead2:
            if pHead2 in hashset:
                return pHead2
            pHead2 = pHead2.next
        return None

####题目37:数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。

# -*- coding:utf-8 -*-
"""
方法1:先用二分查找找到k,然后分别向左向右遍历,统计 x==k的数字个数
    时间 o(logn)+o(m),n为数组长度,m为数字k出现的次数,如果m很大,时间复杂度就是O(n)了
方法2:优化方法1,我们去二分查找k出现的第一个位置和最后一个位置,最后返回两者差值+1。
    找第一个位置:如果arr[middle]==k,再判断前一位是否!=k,如果是返回middle,否则end=middle-1
    找最后一位,与找第一个位置类似,总的时间复杂度O(logn)+O(logn) = O(logn)
"""
class Solution:
    def GetNumberOfK(self, data, k):
        
        # 方法1:二分查找到k后,再向左向右遍历计数
        """
        #定义二分查找函数
        def binarySearch(arr,begin,end,target):
            while begin <= end:
                middle =(begin+end)//2
                if arr[middle] == target:
                     return middle 
                elif arr[middle]> target:
                     end = middle -1
                else: #arr[middle]< target
                     begin = middle +1
            return -1 #如果没找到,返回-1
        
        #鲁棒性判断
        if not data:return 0
        #得到二分查找的结果,如果-1,返回次数为0;如果非负,向左向右计数
        index = binarySearch(data,0,len(data)-1,k)
        if index == -1:return 0
        count = 0 #要返回的计数值
        i, j = index, index-1
        while i<=len(data)-1:
            if data[i] == k:
                count +=1
            i +=1
        while j>=0:
            if data[j] == k:
                count +=1
            j -=1
        return count
        """
        # 方法2:二分查找第一个k后最后一个k
        def binarySearch_first(arr,begin,end,target):
            while begin <= end:
                middle =(begin+end)//2
                if arr[middle] == target:
                    if (middle>0 and arr[middle-1]!=target) or (middle == 0):
                        #确认middle前面不等于k,或者middle已经在0位置,前面没有数了
                         return middle
                    else:
                        end = middle-1
                elif arr[middle]> target:
                     end = middle -1
                else: #arr[middle]< target
                     begin = middle +1
            return -1 #如果没找到,返回-1
        def binarySearch_last(arr,begin,end,target):
            while begin <= end:
                middle =(begin+end)//2
                if arr[middle] == target:
                    if (middle<end and arr[middle+1]!=target) or (middle == end):
                        #确认middle后面不等于k,或者middle已经在end位置,后面没有数了
                         return middle
                    else:
                        begin = middle+1
                elif arr[middle]> target:
                     end = middle -1
                else: #arr[middle]< target
                     begin = middle +1
            return -1 #如果没找到,返回-1
        if not data:
            return 0
        count = 0
        first = binarySearch_first(data,0,len(data)-1,k)
        last = binarySearch_last(data,0,len(data)-1,k)
        if first == -1 or last == -1:
            return 0
        return last -first +1

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

"""
方法1:DFS 在stack中记录到每个结点的长度,在lst_depth中记录每条路径长度,返回lst_depth的最大值
    时间:O(n),空间O(n)
方法2:DFS 优化方法1,不记录每条路径长度,只记录最大路径长度 更加节省空间
方法3:BFS 实现方法1
方法4:BFS 实现方法2
"""
class Solution:
    def TreeDepth(self, pRoot):
        # 方法1:DFS 记录每条路径长度,然后返回最大值
        """
        if not pRoot:#鲁棒性检测
            return 0
        stack = [[pRoot,1]]#结点和到此结点的路径长度
        lst_depth = [] #存放每个路径的长度
        while stack:
            node,depth = stack.pop()
            if node.right:
                stack.append([node.right,depth+1])
            if node.left:
                stack.append([node.left,depth+1])
            if not node.right and not node.left:
                lst_depth.append(depth)
        return max(lst_depth)
        """
        #方法2:DFS 不记录每条路径长度,只记录当前最大路径长度,节省空间
        """
        if not pRoot:#鲁棒性检测
            return 0
        stack = [[pRoot,1]]#结点和到此结点的路径长度
        lst_depth = 1 #存放每个路径的长度
        while stack:
            node,depth = stack.pop()
            if node.right:
                stack.append([node.right,depth+1])
            if node.left:
                stack.append([node.left,depth+1])
            if not node.right and not node.left:
                lst_depth = max(lst_depth,depth)
        return lst_depth
        """
        #方法3:BFS 对应方法1的版本,就不写啦,因为要写方法4
        #方法4:BFS 对应方法2的版本,这里写一下
        if not pRoot:#鲁棒性检测
            return 0
        queue = [[pRoot,1]]#结点和到此结点的路径长度
        lst_depth = 1 #存放每个路径的长度
        while queue:
            node,depth = queue.pop(0)
            if node.right:
                queue.append([node.right,depth+1])
            if node.left:
                queue.append([node.left,depth+1])
            if not node.right and not node.left:
                lst_depth = max(lst_depth,depth)
        return lst_depth

####题目39:平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。

# -*- coding:utf-8 -*-
"""
深度优先遍历二叉树
    某个结点的子树不平衡或者自己不平衡则返回-1
    其他情况返回树深(从叶向上数)
    最后调用DFS函数查看返回值是否为-1,不为-1,则返回True,否则返回False
"""
class Solution:
    def IsBalanced_Solution(self, pRoot):
        #方法1: 深度优先遍历结点,结点不平衡则返回-1
        def dfs(root):
            #这是给叶子结点的
            if root is None:return 0 
            l_depth  = dfs(root.left)
            r_depth = dfs(root.right)
            #左子树不平衡,或右子树不平衡,或当前树结点不平衡 直接返回-1
            if l_depth == -1 or r_depth == -1 or abs(l_depth - r_depth)>1:
                return -1
            return 1 + max(l_depth, r_depth) # 返回当前树的深度
        
        if not pRoot:return True#鲁棒性代码
        return dfs(pRoot) != -1 # 成立说明平衡

####题目40:数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

# -*- coding:utf-8 -*-
"""
方法1:O(n),O(n)
    用一个字典记录每个数字出现的次数,然后遍历找出次数等于1的数字
方法2:O(n),o(1)
    我们知道 两个相同的字符相异或为0,0和其它字符异或等于该字符,且异或满足交换律
    因此,我们把整个数组异或,会得到两个单次字符异或的结果,这个结果二进制中至少有一个1
    找到结果中1出现的任何一个位置,按此位是否为1,将数组可分为两组,一组为0,一组为1
    那么两个单次字符必然分别出现两组中,而两组中其它数字都是重复出现的
    因此,对这两组中的字符异或,得到的两个结果就是 这两个出现一次的数字
"""
class Solution:
    def FindNumsAppearOnce(self, array):
        #方法2:异或
        #函数,判断num中的idx位(从低向高)是否为1
        def Bit_equalone(num, idx):
            num = num >> idx
            return num & 1
        #鲁棒性检测
        if not array: return [0,0]
        #数组array直接异或,得到两个单次字符异或的结果
        temp = 0
        for ch in array:
            temp ^= ch
        #找到结果中,1出现的任意一个位置(我们取最低位)
        index = 0
        while (temp&1) == 0:
            index += 1
            temp = temp >> 1
        #将数组array按index位是否为1分为两组,分别异或
        res1, res2 = 0, 0
        for ch in array:
            if Bit_equalone(ch,index):
                res1 ^= ch
            else:
                res2 ^= ch
        return [res1,res2]#返回两个只出现一次的数
        #方法1:字典
        """
        if not array: return 0,0
        dic = {}
        res = [0]*2
        for ch in array:
            dic[ch] = dic.get(ch,0)+1
        i=0
        for ch,count in dic.items():
            if count == 1:
                res[i]=ch
                i += 1
        return res[0],res[1]
        """

第四期内容就到这啦~

本文章所有代码及文档均以上传至github中,感谢您的star,fork and rp.

github链接:https://github.com/LSayhi
仓库链接:https://github.com/LSayhi/Algorithms

CSDN链接:https://blog.csdn.net/LSayhi

微信公众号:AI有点可ai
AI有点可ai.jpg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值