【剑指offer】解题思路 39-52


备忘:
46题,没思路
47题,循环解法没写出来(写错了)

第五章 优化时间和空间效率

5.2 时间效率

 

面试题39:数组中出现次数超过一半的数字

【题目描述】
数组中一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为 9 的数组{1,2,3,2,2,2,5,4,2}。由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2.

【方法1:O(n)】
快速排序的思想,找到数组中第K大的数字的思路,出现次数超过一半的数字一定在第n/2位置上。

【方法2:O(n)】
题目中要找的数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数的和还要多。
因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。当我们遍历到下一个数字的时候,

  • 如果下一个数字和当前我们保存的数字相同,则次数加 1;
  • 如果和当前我们保存的数字不同,则次数减 1;
  • 当次数减到 0 的时候,我们将保存的数字改为当前遍历所处的位置,并将次数更改为 1。

 

面试题40:最小的k个数

【题目描述】
输入n个整数,找出其中最小的k个数。例如输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

方法一:基于Partition的方法,复杂度为O(n),必须要修改输入数组

具体思路:通过快排的Partition函数,获取排序的主元位置r,判断r与k的大小关系,如果r=k或者r=k-1,直接返回前半部分的nums[:r]即可(为什么r=k或r=k-1都可以呢,因为主元前面的元素是最小的前k个,主元元素是最小的第k+1个,都已经找到了),如果r>k,对前半部分使用Patition函数,即可。

方法二:最大堆方法,复杂度为O(nlogk),无需修改输入数组, 适合海量数据

具体思路:维护一个含有k个元素的最大堆,最大堆的顶部为最大堆中最大的元素,遍历数组的元素,如果该元素比最大堆顶部的值还要大,则肯定不可能是最小的k个数,直接跳过,如果该元素小于最大堆顶部的元素,那么用这个元素替换堆顶元素,并且ShiftDown,找到该元素的合适位置。

两种解法的比较:
在这里插入图片描述
 

面试题41:数据流中的中位数

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

【解题思路】
用一个最大堆来实现左边的数据容器,用一个最小堆来实现右边的数据容器,往堆中插入一个元素的复杂度为O(logn),由于只需要O(1)的时间就可以得到堆顶数据,因此得到中位数的复杂度为O(1)

【两个细节问题需要注意:】

  1. 要保证最大堆和最小堆中的数据平衡(长度之差不超过1),为了保证平均分配,可以在数据总数为偶数时,把新数据插入最小堆,在数据为奇数时,把新数据插入最大堆。
  2. 要保证最大堆中的所有元素都小于最小堆中的元素,因此会有特殊情况出现。当数据总数为偶数时,我们把新数据插入了最小堆,但这个新数据比最大堆中的一些数据小,此时应该把最大堆中的堆顶元素(最大元素)pop出来,append到最小堆中去,然后再把这个新数据append到最大堆中,再通过ShiftUp操作分别维护这两个堆。同理,当本来应该添加到最大堆中的新数据比最小堆中的一些元素还要大时,也要执行相应的操作。

 

面试题42:连续子数组的最大和

【解题思路】:
遍历数组nums一遍即可实现,维护两个变量,一个是累加过程中的子数组和sum,一个保存的是最大的子数组和res。
判断条件:
如果sum < 0,那么相当于前面累加部分的结果都将被抛弃。
如果sum >=0,那么sum = sum + nums[i],并且不断更新res,保证res = max(res, sum)

 

面试题43:1~n整数中1出现的次数

【题目描述】:
题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。

【解题思路】:
采用递归的思路解决,将整数转换成字符串型,例如输入21345,拆成1-1345和1346-21345两部分,先来处理第二部分(第一部分可以递归处理):

  1. 首先分析1出现在最高位的次数:
    最高位大于1时,1出现在最高位(这里是万位)的次数共有10^4=10000次(当最高位等于1时(例如12345),1出现在最高位的次数为2345 + 1=2346次)
  2. 然后分析1出现在除最高位之外的其它四位数中:
    由于最高位是2,因此可以分为两段,1346-11345和11346-21345,每一段剩下的4位数字中,选择其中一位是1,其余三位可以在0~9这十个数字中任意选择,因此总共出现的次数为 2 × 4 × 1 0 3 = 8000 2×4×10^3=8000 2×4×103=8000次。

第一部分1-1345可以递归处理。

【注意】边界条件的处理,例如输出0、1等。
 

面试题44:数字序列中某一位的数字

【题目描述】:
数字以01234567891011121314…的格式排列。在这个序列中,第5位(从0开始计)是5,第13位是1,第19位是4。求任意第n位对应的数字。

【解题思路】:
把序列根据数字的位数分成段,比如前10位是0-9这10个只有一位的数字,接下来的180位是90个10-99的两位数,接下来的2700位是 900个100-999的三位数,以此类推。
while循环,先找n在哪一段,然后找具体的那一位。

 

面试题45:把数组排成最小的数(重要)

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

【解题思路】:
为了防止数组拼接后越界,提前将所有数字都转成字符串处理。
这题的关键点在于,重新定义一个compare函数,用于比较数组中的数字,这个compare函数应该比较的是谁排在前面,而不是单纯的比较数字的大小。
https://blog.csdn.net/m0_38088359/article/details/82957670

自定义排序函数,用来修改sort()函数的key,使用functools 里的cmp_to_key

from functools import cmp_to_key # 调用cmp_to_key函数
def PrintMinNumber(nums):
     if not nums:
         return ""
     nums = list(map(str,nums))
     nums.sort(key=cmp_to_key(compare)) # 自定义compare函数
     return ''.join(i for i in nums).lstrip('0') # lstrip用于截掉字符串左边的空格或指定字符

def compare(s1, s2):
    if s1 + s2 <= s2 + s1:
        return -1
    else:
        return 1

 

面试题46:把数字翻译成字符串

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

【解题思路】
递归/动态规划问题。从最小的问题开始,自下而上解决问题,可以避免重复,不断累加后面的结果,就可以逆序推出前面的结果。
https://blog.csdn.net/mabozi08/article/details/88931733
 

面试题47:礼物的最大价值(动态规划)

【题目描述】
在一个 m*n 的棋盘中的每一个格都放一个礼物,每个礼物都有一定的价值(价值大于0).你可以从棋盘的左上角开始拿各种里的礼物,并每次向右或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘及上面个的礼物,请计算你最多能拿走多少价值的礼物?

【解题思路】
动态规划题,可以使用递归或循环的方法来求解(循环解法效率更高)。对于(x,y)处,一定有两种方法到达,从(x-1,y)或者(x,y-1)。如果要得到最大的礼物价值,上一步的时候肯定要选最大的。也就是max(f(left),f(up))。在具体的实现里面,这里构建了一个二维数组来存储各个位置所能得到的最大值。

还有改进版,将保存礼物最大价值的数组由二维压缩成一维数组来存储,一位数组的长度为列数,该数组前面j个数字分别是当前行(第i行)前面j个格子礼物的最大价值,之后的数字分别保存上一行(第i-1行)n-j个格子礼物的最大价值。

 

面试题48:最长不含重复字符的子字符串(动态规划)(重要)

【题目描述】:
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含从’a’到’z’的字符。例如,在字符串中”arabcacfr”,最长非重复子字符串为”acfr”,长度为4。

【解题思路】:(参考
使用动态规划,记录当前字符之前的最长非重复子字符串长度f(i-1),其中i为当前字符的位置。每次遍历当前字符时,分两种情况:

1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。若d大于f(i-1),即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。若d小于或等于f(i-1),即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。

 

5.3 时间效率与空间效率的平衡

 

面试题49:丑数

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

【解题思路】:

【笨办法】:
使用遍历法求第k个丑数,从1开始遍历,如果是丑数则count++,直到count=k为止。那么如何判断丑数呢?根据丑数的定义,丑数只有2,3,5这三个因子,那么我们就拿数字除以这三个因子。具体算法如下:

Step1.如果一个数能够被2整除,那么让他继续除以2;

Step2.如果一个数能够被3整除,那么让他继续除以3;

Step3.如果一个数能够被5整除,那么让他继续除以5;

Step4.如果最后这个数变为1,那么这个数就是丑数,否则不是。

缺陷:所有整数都需要计算,即使一个数字不是丑数,我们还是需要对它做求余数和除法操作。因此该算法的时间效率不是很高,可以改进该算法,只计算丑数,而不在非丑数的整数上浪费时间。

【改进方法】:
假设数组中已经有若干个排好序的丑数,并且把已有的最大丑数记为M,接下来找下一个丑数。该丑数肯定是前面的某一个丑数乘以2、3或5的结果。

我们把得到的第一个丑数乘以2以后得到的大于M的结果记为M2。同样,我们把已有的每一个丑数乘以3和5,能得到第一个大于M的结果M3和M5。那么M后面的那一个丑数应该是M2,M3和M5当中的最小值:Min(M2,M3,M5)。所以我们需要维护三个变量p2,p3,p5,这三个变量分别指向的是数组中乘以2、3、5之后大于M的第一个数。

 

面试题50:第一个只出现一次的字符

【题目描述】:
在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出’b’。要求时间复杂度为O(n)。

【解题思路】:
维护一个哈希表,哈希表的key是字符,value是该字符出现的次数。第一次遍历时构造哈希表;接下来第二次遍历的时候,每扫描到一个字符时,就能从哈希表中得到该字符出现的次数,第一个只出现一次的字符就是需要的结果。

相关题目
相关题目一:从第一个字符串中删除在第二个字符串中出现过的所有字符

定义一个函数,输入两个字符串,从第一个字符串中删除在第二个字符串中出现过的所有字符。例如从第一个字符串“We are students.”中删除在第二个字符串"aeiou"中出现过的字符串得到的结果是"W r Stdnts."。

【解题思路】:

我们可以创建一个用数组实现的简单哈希表来存储第二个字符串。这样我们从头到尾扫描第一个字符串的每一个字符时,用O(1)时间就能判断出该字符是不是在第二个字符中。如果第一个字符串的长度是n,那么总的时间复杂度是O(n)。

相关题目二:删除字符串中所有重复出现的字符

定义一个函数,删除字符串中所有重复出现的字符。例如输入"google",删除重复的字符串之后的结果是"gole"。

【解题思路】:

我们可以创建一个用布尔型数组实现的简单的哈希表。数组中的元素的意义是其下表看作ASCII码对应的字母在字符串中是否已经出现。我们先把数组中所有的元素都设为false。以google为例,当扫描到第一个g时,g的ASCII码是103,那么我们把数组下标为103的元素设为true,就知道g在前面已经出现了。即我们用O(1)时间就能判断出每个字符是否在前面已经出现过。如果是字符串的长度是n,那么总的时间复杂度是O(n)。

相关题目三:判断输入的两个字符串是不是互为变位词

在英语中,如果两个单词中出现的字母相同,并且每个字母出现的次数也相同,那么这两个单词互为变位词(Anagram)。例如silent与listen、evil与live等互为变位词。完成一个函数,判断输入的两个字符串是不是互为变位词。

【解题思路】:

我们可以创建一个用数组实现的简单哈希表,用来统计字符串中每个字符出现的次数。当扫描到第一个字符串中的每个字符时,为哈希表对应的项的值增加1。接下来扫面第二个字符串,扫描到每个字符串时,为哈西边对应的项的值减去1。如果扫描完第二个字符串后,哈希表所有的值都是0,那么这两个字符串就互为变位词。

题目二:字符流中第一个只出现一次的字符

【题目描述】:
实现一个函数,找出字符流中只出现一次的字符,例如,当字符流中只读出前两个字符时“go”时,第一个只出现一次的字符时“g”,当从字符流中读出前6个字符“google“时,第一个只出现一次的字符时”l“。

【解题思路】:
维护一个哈希表,哈希表的key为字符,字符对应的位置下标作为value,每次扫描到一个字符时,先判断这个字符对应的key是否已经在哈希表中了,如果已经在了,则说明该字符已经出现过了,不符合要求,将该字符对应的value设为-1,如果该字符对应的key不在哈希表中,则插入key,并且将value设为位置下标。
遍历哈希表,找到value值>=0的,且value最小的key,就是要输出的字符。

 

面试题51:数组中的逆序对(归并排序)

【题目描述】:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。例如,在数组{7,5,6,4},逆序对总共有5对,{7,5},{7,6},{7,4},{5,4},{6,4};

【解题思路】:
先把数组分成子数组,统计出子数组内部的逆序对数目,然后再统计两个相邻子数组之间的逆序对的数目。
在这里插入图片描述

利用归并排序的思路,对数组进行分组,分成left和right两部分,组内进行排序,然后将排好序的left和right两部分再merge起来。 分组和排序的过程与归并排序完全相同,区别仅仅在于合并的部分,假定此时left和right两个子数组已经排好序了,分别使用两个指针指向left和right的尾元素,并比较两个指针指向的数字。如果left中的数字大于right中的数字,则构成逆序对,且逆序对的数目等于right数组中剩余元素的个数。如果left中的数字小于right中的数字,则不构成逆序对。每次比较时,将left和right中更大的数字添加进result列表中,并且将指针往前移,进行下一轮比较。

 

面试题52:两个链表的第一个公共节点

【题目描述】:
输入两个链表,找出它们的第一个公共结点。

【解题思路】:
方法1(蛮力法):
在第一个链表上顺序遍历每个节点,每遍历到一个节点,就在第二个链表上顺序遍历每个节点,如果第二个链表上有一个节点和第一个链表上的节点一样,说明两个链表在这个节点上重合,返回该节点即可。时间复杂度O(mn)(假设两个链表的长度分别为m和n)

方法2(栈):
将两个链表的节点分别存入两个栈中,然后从两个栈中分别弹出一个节点,如果两个节点相同,则弹出并进行下一轮,直到找到最后一个相同的节点。时间复杂度O(m+n),空间复杂度O(m+n)。

方法3:
首先遍历两个链表的长度,并且知道了长链表与短链表的长度差d。第二次遍历时,长链表先走d步,然后两个链表一起向前走,直到指向相同给节点为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值