python code practice(二):KMP算法、二分搜索的实现、哈希表
关于KMP---一个很有名的字符串匹配算法,请参考上面链接学习。
1、替换空格
题目描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
分析:
将长度为1的空格替换为长度为3的“%20”,字符串的长度变长。 如果允许我们开辟一个新的数组来存放替换空格后的字符串, 那么这道题目就非常简单。设置两个指针分别指向新旧字符串首元素, 遍历原字符串,如果碰到空格就在新字符串上填入“%20”, 否则就复制元字符串上的内容。但是如果面试官要求 在原先的字符串上操作,并且保证原字符串有足够长的空间来存放替换后的字符串,那么我们就得另想方法。
class Solution: def replaceSpace(self, s): s = s.replace(' ', '20%') return s s = Solution() print(s.replaceSpace('We Are Happy'))
方法2:
首先遍历原字符串,找出字符串的长度以及其中的空格数量, 根据原字符串的长度和空格的数量我们可以求出最后新字符串的长度。 设置两个指针point1和point2分别指向原字符串和新字符串的末尾位置。 (这里为什么取末尾开始遍历,而不是取起始开始遍历,是为了利用point1==point2这个判断条件) 如果point1指向内容不为空格,那么将内容赋值给point2指向的位置, 如果point1指向为空格,那么从point2开始赋值“02%” 直到point1==point2时表明字符串中的所有空格都已经替换完毕。
class Solution: def replaceSpace(self, oldStringList): blankNumber = 0 #空格数量 oldStringLen = len(oldStringList) #遍历原字符串,统计字符串中的空格数量 for i in range(oldStringLen): if oldStringList[i] == ' ': blankNumber += 1 #计算新字符串所需要的长度 newStringLen = oldStringLen + blankNumber*2 #声明新字符串列表 newStringList = [' '] * newStringLen #设置两个指针,分别指向原字符串和新字符串的末尾位置 point_old = oldStringLen - 1 point_new = newStringLen - 1 #遍历替换 while point_old != point_new: #如果两个指针位置不同,表示没有替换完成 if oldStringList[point_old] != ' ': newStringList[point_new] = oldStringList[point_old] point_old -= 1 point_new -= 1 else: newStringList[point_new] = '0' newStringList[point_new-1] = '2' newStringList[point_new-2] = '%' point_old -= 1 point_new -= 3 #指针恰好相同时,将之前的字符也补上 if point_old > 0: for i in range(point_old, -1, -1): newStringList[i] = oldStringList[i] #将字符串数组/列表组合为字符串 newString = '' for i in range(newStringLen): newString += str(newStringList[i]) return newString s = Solution() print(s.replaceSpace('We Are Happy'))
2、正则表达式匹配
请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。
这道题需要把题意首先仔细研究清楚。
先分享一个较为清晰的思路:
前提条件:'.'表示任意一个字符;'*'表示它前面的字符可以出现任意次(包含0次)。
首先,考虑特殊情况:
1)两个字符串都为空,返回true
2)当第一个字符串不空,而第二个字符串空了,返回false(因为这样,就无法匹配成功了,而如果第一个字符串空了,第二个字符串非空,还是可能匹配成功的,比如第二个字符串是“a*a*a*a*”,由于‘*’之前的元素可以出现0次,所以有可能匹配成功)
之后就开始匹配第一个字符,这里有两种可能:匹配成功或匹配失败。但考虑到pattern下一个字符可能是‘*’, 这里我们分两种情况讨论:pattern下一个字符为‘*’或不为‘*’:
1)pattern下一个字符不为‘*’:
这种情况比较简单,直接匹配当前字符。如果匹配成功,继续匹配下一个;如果匹配失败,直接返回false。注意这里的“匹配成功”,除了两个字符相同的情况外,还有一种情况,就是pattern的当前字符为‘.’, 同时str的当前字符不为‘\0’。
2)pattern下一个字符为‘*’:
pattern下一个字符为‘*’时,稍微复杂一些,因为‘*’可以代表0个或多个。这里把这些情况都考虑到:
a)当‘*’匹配0个字符时,str当前字符不变,pattern当前字符后移两位,跳过这个‘*’符号;
b)当‘*’匹配1个或多个时,str当前字符移向下一个,pattern当前字符不变。(这里匹配1个或多个可以看成一种情况,因为:当匹配一个时,由于str移到了下一个字符,而pattern字符不变,就回到了上边的情况a;当匹配多于一个字符时,相当于从str的下一个字符继续开始匹配)
C++ code:
class Solution { public: bool match(char* str, char* pattern) { if(*str=='\0' && *pattern=='\0') return true; if(*str!='\0' && *pattern=='\0') return false; if(*(pattern+1)!='*'){ if(*str==*pattern || (*str!='\0' && *pattern=='.')) return match(str+1,pattern+1); else return false; } else{ if(*str==*pattern || (*str!='\0' && *pattern=='.')) return match(str,pattern+2) || match(str+1,pattern); else return match(str,pattern+2); } } };
python版本:
思路:当模式中的第二个字符是“*”时:
如果字符串第一个字符跟模式第一个字符不匹配,则模式后移2个字符,继续匹配。如果字符串第一个字符跟模式第一个字符匹配,可以有3种匹配方式:
1.模式后移2字符,相当于x*被忽略;
2.字符串后移1字符,模式后移2字符,相当于x*匹配一位;
3.字符串后移1字符,模式不变,即继续匹配字符下一位,相当于x*匹配多位;
当模式中的第二个字符不是“*”时:
如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的部分。
如果字符串第一个字符和模式中的第一个字符相不匹配,直接返回False。
def match(self, s, pattern): # write code here #如果两者都为空,则匹配成功 if (len(s) == 0 and len(pattern) == 0): return True #如果模式为空,字符串不为空,则匹配不成功 if (len(s) > 0 and len(pattern) == 0): return False if len(pattern) > 1 and pattern[1] == '*': if s and (pattern[0] == '.' or s[0] == pattern[0]): f1 = self.match(s[1:], pattern)#多个 f2 = self.match(s[1:], pattern[2:])#一个 f3 = self.match(s, pattern[2:])#零个 if f1 or f2 or f3: return True else: return False else: return self.match(s, pattern[2:]) elif s and (pattern[0] == '.' or s[0] == pattern[0]): return self.match(s[1:], pattern[1:]) #如果字符串为空,模式不为空,但模式长度等于1,或者模式长度大于1但第二个字符不为’*‘,则匹配不成功 else: return False
另一个:
这道题边界情况也有点多,首先判断s和pattern的长度,分出了四种情况,其中
1.如果s与pattern都为空,则True;
2.如果s不为空,而pattern为空,则False;
3.如果s为空,而pattern不为空,判断pattern是否是a...这种情况,*可以代表0次,这样一来可以将pattern往后移两位再进行match递归;
4.如果s、pattern不为空,又可以分为两种情况:
4.1.如果pattern的第二个字符不为*时,如果s[0]与pattern[0]能匹配上就将s和pattern都往后移1位再进行match,否则不匹配为False;
4.2.如果pattern的第二个字符为*时,如果s[0]与pattern[0]匹配不上,则将pattern后移2位再进行match;如果s[0]与pattern[0]能匹配上,会出现三种情况,分别是pattern[1] = '\'的*代表的三种情况0、1或多个,分别对应pattern后移2位s不变、pattern后移2位,s后移1位、pattern不变s后移1位,这三种情况都有可能出现所以用or或运算连接三种情况的递归。
讲的比较麻烦,不直观,直接看代码吧。
# -*- coding:utf-8 -*- class Solution: # s, pattern都是字符串 def match(self, s, pattern): # write code here len_s = len(s) len_pattern = len(pattern) # 如果s与pattern都为空,则True if len_s == 0 and len_pattern == 0: return True # 如果s不为空,而pattern为空,则False elif len_s != 0 and len_pattern == 0: return False # 如果s为空,而pattern不为空,则需要判断 elif len_s == 0 and len_pattern != 0: # pattern中的第二个字符为*,则pattern后移两位继续比较 if len_pattern > 1 and pattern[1] == '*': return self.match(s, pattern[2:]) else: return False # 如果s不为空,pattern也不为空,则需要判断 else: # pattern的第二个字符为*的情况 if len_pattern > 1 and pattern[1] == '*': # s与pattern的第一个元素不同,则s不变,pattern后移两位,相当于pattern前两位当成空 if s[0] != pattern[0] and pattern[0] != '.': return self.match(s, pattern[2:]) # 如果s[0]与pattern[0]相同,且pattern[1]为* else: # 会有三种情况 # pattern后移2个,s不变;相当于把pattern前两位当成空,匹配后面的,把*当做0次 F1 = self.match(s, pattern[2:]) # pattern后移2个,s后移1个;相当于pattern前两位与s[0]匹配,把*当做1次 F2 = self.match(s[1:], pattern[2:]) # pattern不变,s后移1个;相当于pattern前两位,与s中的多位进行匹配,把*当做多次 F3 = self.match(s[1:], pattern) # 有一个为真就能返回真值 return F1 or F2 or F3 # pattern的第二个字符不为*的情况 else: # s和pattern的第一个字符匹配上了,都往后移1位 if s[0] == pattern[0] or pattern[0] == '.': return self.match(s[1:],pattern[1:]) else: return False
3、表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
思路:首先要想到所有的情况,然后进行分类讨论。-123.45e-67
1、+-号后面必定为数字或后面为.(-.123 = -0.123)
2、+-号只出现在第一位或在eE的后一位
3、.后面必定为数字或为最后一位(233. = 233.0)
4、eE后面必定为数字或+-号
# -*- coding:utf-8 -*- class Solution: # s字符串 def isNumeric(self, s): # write code here # 标记符号、小数点、e是否出现过 sign = False decimal = False hasE = False for i in range(len(s)): if (s[i] == 'e' or s[i] == 'E'): # e后面一定要接数字 if (i == len(s)-1): return False # 不能同时存在两个e if (hasE == True): return False hasE = True elif (s[i] == '+' or s[i] == '-'): # 第二次出现+-符号,则必须紧接在e之后 if (sign and s[i-1] != 'e' and s[i-1] != 'E'): return False # 第一次出现+-符号,且不是在字符串开头,则也必须紧接在e之后 elif (sign == False and i > 0 and s[i-1] != 'e' and s[i-1] != 'E'): return False sign = True elif (s[i] == '.'): # e后面不能接小数点,小数点不能出现两次 if (hasE or decimal): return False decimal = True # 非法字符 elif(s[i] < '0' or s[i] > '9'): return False return True
4、字符流中第一个不重复的字符
题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述:
如果当前字符流没有存在出现一次的字符,返回#字符。
分析:
这题其实并不难,里面需要唯一注意的点就是我们需要返回的字符是 第一次只出现一次的字符, 所以当我们用map存储的时候,因为map是乱序的,所以我们需要额外判断我们返回的字符在字符流中的序号是不是最小。
这题分三步:
新建一个string input用来存储我们接收到的所有字符,同时也能给我们后续判断字符出现的顺序做参照,再新建一个hashmap,用来存储每个字符我们接收过的次数。
insert function 填写: 我们首先判断hashmap的keyset里有没有当前收到字符,没有我们需要把keyset更新,有的话我们需要把对应的value更新,同时我们将收到的字符串放进前面我们新建的string input里。
FirstAppearingOnce function 填写: 我们需要先新建一个int index,来储存我们现在找到的最小的只出现一次的字符的index,然后我们新建一个char result,因为题目里提到,如果没有找到符合的字符,我们需要返回“#”,所以我们将char result设为默认值“#”。接下来我们遍历整个hashmap,如果有只出现一次的字符,我们记录下它的index,如果小于我们创建的int index,我们更新int index,同时更新我们对应的result。最后,我们return result即可。
根据该思路写的java code:
import java.util.*; public class Solution { //Insert one char from stringstream String input = ""; Map<Character,Integer> map = new HashMap<>(); public void Insert(char ch) { if(!map.keySet().contains(ch)){ map.put(ch,1); }else{ map.put(ch,map.get(ch)+1); } input += ch; } //return the first appearence once char in current stringstream public char FirstAppearingOnce() { int index = Integer.MAX_VALUE; char result = '#'; for(Character c: map.keySet()){ if(map.get(c) == 1){ if(input.indexOf(c) < index){ index = input.indexOf(c); result = input.charAt(index); } } } return result; } }
贴一个python的:
# -*- coding:utf-8 -*- class Solution: def __init__(self): self.s='' self.dict={} #创建字典,key为读取的字符串中的每一个字符,val为每个字符出现的个数的计数值 # 返回对应char def FirstAppearingOnce(self): # write code here for i in self.s: #遍历字符串s中的字符 if self.dict[i]==1: #如果某个字符对应的计数为1,则返回该字符 return i return '#' #在所有字符遍历完后,进行判断 def Insert(self, char): # write code here self.s=self.s+char #从字符流中读入字符到字符串s中 if char in self.dict: self.dict[char]=self.dict[char]+1 #如果读入的字符在字符串中已存在,在字典中对应的字符计数加一 else: self.dict[char]=1 #如果读入的字符在字符串中不存在,则字典中对应的字符计数为一(即新增了一个新的字符)
5、二分搜索
leetcode 69. x的平方根
题目描述:
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
分析:基本不等式+二分法
- 基本不等式(a+b)/2 >=√ab 推导自 (a-b)^2 >= 0,注意 a>0 且 b>0
class Solution: def mySqrt(self, x: int) -> int: r = x while r*r > x:
r = (r + x/r) // 2 return int(r)
class Solution: def mySqrt(self, x: int) -> int: l, h = 0, x while l < h: m = (l + h) // 2 if m**2 <= x < (m+1)**2: return m elif m**2 < x: l = m + 1 else: h = m - 1 return l
方法1:库函数
def mySqrt(self, x): """ :type x: int :rtype: int """ return int(math.sqrt(x))
方法2:二分法
class Solution: def mySqrt(self, x: int) -> int: left = 0 right = math.ceil(x / 2) #math.ceil(x)返回大于等于参数x的最小整数,即对浮点数向上取整 res = 0 while left <= right: mid = left + (right - left) // 2 tmp = mid * mid if tmp == x: return mid elif tmp < x: left = mid + 1 else: right = mid - 1 return right
方法3:
class Solution: def mySqrt(self, x: int) -> int: r = x while r * r > x: r = (r + x // r) // 2 return r
6、哈希表
leetcode 1:两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
方法1:最直接也是最笨的方法,用两个for循环
拿数组里的第一个数字和后面的数字逐一分别相加,看是否等于target;如果不等于target,那么就继续拿数组里的第二个数字和后面的数字相加;不停的去一个个试...直到等于target,返回这2个数字所在的下标。代码非常浅显直接易懂:
class Solution: def twoSum(self,nums,target): n = len(nums) # 获取nums的长度,是4 for x in range(n): # 外层循环先取出下标0,对应着数组里的第一个数字 for y in range(x+1,n): # 内层循环取出下标1,对应着数组里的第二个数字 if nums[x] + nums[y] == target: # 如果第一个数字+第二个数字=target return x,y # 上面的判断是对的话,那么就返回下标 break # 并停止程序 else: # 如果上面的条件不满足的话,内层for循环就会继续取出下标2进行判断...如果都不满足,那么外层for循环就会取出下标1...依次类推 continue
方法2:直接用target 减去 取出的数字,看结果有没有在数组里。用一个for循环。
class Solution: def twoSum(self,nums,target): n = len(nums) for x in range(n): a = target - nums[x] if a in nums: # 判断a有没有在nums数组里 y = nums.index(a) # 有的话,那么用index获取到该数字的下标 if x == y: continue # 同样的数字不能重复用,所以这里如果是一样的数字,那么就不满足条件,跳过 else: # 否则就返回结果 return x,y break else: continue # 上面的条件都不满足就跳过,进行下一次循环
方法3:用字典。
class Solution: def twoSum(self,nums,target): d = {} n = len(nums) for x in range(n): if target - nums[x] in d: return d[target-nums[x]],x else: d[nums[x]] = x
另附:
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: hashmap = {} for idx, num in enumerate(nums): if target - num in hashmap: return [hashmap[target - num],idx] else: hashmap[num] = idx
时空复杂度均为O(N)。
7、旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
思路:二分查找
保证rotateArray[left]为全场最小,当rotateArray[left]<rotateArray[right]时,证明进入了有序数组,直接输出。
# -*- coding:utf-8 -*- class Solution: def minNumberInRotateArray(self, rotateArray): # write code here if len(rotateArray) == 0: return 0 left = 0 right = len(rotateArray)-1 #right=5-1=4 while left < right: #left:0 right:4 if rotateArray[left] < rotateArray[right]: #3<2 不成立 return rotateArray[left] mid = left + (right-left)//2 #mid=0+(4-0)//2=0+2=2 #左边有序 取另一半 if rotateArray[left] < rotateArray[mid]: #3<5 成立 left = mid+1 #left=2+1=3 #右边有序 右边取最小 elif rotateArray[mid] < rotateArray[right]: #5<2 不成立 right = mid #right=2 #前面两个相等的时候,left加一继续 else : left += 1 return rotateArray[left] #返回rotateArray[3],即1.
8、左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
1、C++
class Solution { public: string LeftRotateString(string str, int n) { if(str.length() == 0) //考虑string为空的情况 return str; n %= str.size(); // 仔细想一想,n有可能比字符串的长度大,这个时候还是可能发生越界,但是这题的案例应该没设计好,没有暴露问题!如果n大于str.length(),左移n位其实就相当于左移n % str.length()位。 string str1 = str.substr(0,n); string str2 = str.substr(n); return str2+str1; } };
注意:如果想到了n可能大于字符串的长度,却没有想到字符串可能为空,那么n %= str.length()就会报浮点错误:您的程序运行时发生浮点错误,比如遇到了除以 0 的情况的错误。记得考虑全面些。
2、python版本
# -*- coding:utf-8 -*- class Solution: def LeftRotateString(self, s, n): # write code here if s == '': return s ls = list(s) l = [] for i in range(n): #这个for循环,依据左移数目n,做n次循环。每次将原字符串s中的首位字符取出,放在新的字符串列表l中。并同时在原字符串中删去已取出的该字符。最后将取出的字符append到删减后的原字符串在中。 l.append(ls[0]) del ls[0] ls.append(l[i]) return "".join(ls) s = Solution() print(s.LeftRotateString('abcXYZdef', 3))
9、字符串的排列
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
描述:
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
# -*- coding:utf-8 -*- class Solution: def Permutation(self, ss): # write code here length = len(ss) if length <= 1: return ss lists = [] for i in range(length): first_str = ss[i] # 这里的ss[:i]+ss[i+1:] 刚好把ss[i]抠出来,确定a/b/c后,删除,下次递归使用剩余元素 for temp_sub_list in self.Permutation(ss[:i]+ss[i+1:]): temp = first_str + temp_sub_list if temp not in lists: lists.append(temp) return lists s = Solution() print(s.Permutation('abc'))
class Solution: def Permutation(self, ss): # write code here if not ss: return [] res = [] def backtrack(nums, tmp): if not nums: res.append(tmp) return for i in range(len(nums)): backtrack(nums[:i] + nums[i+1:], tmp + nums[i]) backtrack(ss, '') return sorted(list(set(res))) s = Solution() print(s.Permutation('abc'))
10、第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
方法1:用python的count方法
# -*- coding:utf-8 -*- class Solution: def FirstNotRepeatingChar(self, s): # write code here if not s: return -1 for i in range(len(s)): if s.count(s[i]) == 1: return i return -1
方法2:建立一个哈希表,第一次扫描的时候,统计每个字符的出现次数。第二次扫描的时候,如果该字符出现的次数为1,则返回该字符的位置。时间复杂度O(2n)。
# -*- coding:utf-8 -*- class Solution: def FirstNotRepeatingChar(self, s): # write code here length = len(s) if length == 0: return -1 str_hash = {} for i in range(length): if s[i] in str_hash: str_hash[s[i]] += 1 else: str_hash[s[i]] = 1 for i in range(length): if str_hash[s[i]] == 1: return i return -1
11、在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
分析:
本题是一个典型的二分查找类题目。题目的意思很明确,就是寻找左边界和右边界。
二分查找看似很简单,其实里面的变化还是蛮多的。
如何寻找左边界呢?
1.1如果nums[mid] < target,那么左边没有target。start = mid+1,
1.2如果nums[mid]==target,在正常的二分查找中,这种情况是可以直接返回结果。但是因为我们是要查找左边界,这就有可能mid的左边还有可能有target这个数。因此需要进行两步操作。
1.2.1 用一个变量res标记mid的值,如果左边还有数值等于target,则更新res的值。否则直接返回res即可。
1.2.2 end = mid - 1,继续向左查找。
1.3 如果nums[mid] > target, 同1.2.2 end = mid - 1,继续向左查找。
如何寻找右边界呢?
2.1如果nums[mid] > target,那么右边没有target。end = mid - 1,
2.2如果nums[mid]==target,在正常的二分查找中,这种情况是可以直接返回结果。但是因为我们是要查找右边界,这就有可能mid的右边还有可能有target这个数。因此需要进行两步操作。
2.2.1 用一个变量res标记mid的值,如果左边还有数值等于target,则更新res的值。否则直接返回res即可。
2.2.2 start = mid + 1,继续向右查找。
2.3 如果nums[mid] < target, 同1.2.2 start = mid + 1,继续向右查找。
其实说白了,找target,找target的左边界或者找target的右边界仅仅是在nums[mid]==target的处理上有差别。
class Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: if not nums or len(nums) == 0: return [-1, -1] left, right = -1, -1 # left bounder start, end = 0, len(nums) - 1 while start + 1 < end: mid = start + ((end - start) >> 1) if target <= nums[mid]: end = mid else: start = mid if target == nums[end]: left = end if target == nums[start]: left = start # right bounder start, end = 0, len(nums) - 1 while start + 1 < end: mid = start + ((end - start) >> 1) if target >= nums[mid]: start = mid else: end = mid if target == nums[start]: right = start if target == nums[end]: right = end return [left, right]
class Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: # 取起始下标 l, r = 0, len(nums) - 1 while l < r: mid = (l + r) // 2 if nums[mid] >= target: r = mid else: l = mid + 1 # 没找到 if not nums or nums[l] != target: return [-1,-1] # 取结束下标 a, b = l, len(nums) - 1 while a < b: mid = (a + b + 1) // 2 if nums[mid] <= target: a = mid else: b = mid - 1 return [l,a]
12、找到k个最接近的元素
给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。
示例 1:
输入: [1,2,3,4,5], k=4, x=3
输出: [1,2,3,4]
示例 2:
输入: [1,2,3,4,5], k=4, x=-1
输出: [1,2,3,4]
说明:
k 的值为正数,且总是小于给定排序数组的长度。
数组不为空,且长度不超过 104
数组里的每个元素与 x 的绝对值不超过 104
方法1:双指针
由于数组已经排序,完全可以从左右两个方向向中间遍历,每次找到距离大的那个数字,弹出去就好了,最后保留k个数字。
class Solution(object): def findClosestElements(self, arr, k, x): """ :type arr: List[int] :type k: int :type x: int :rtype: List[int] """ while len(arr) > k: if x - arr[0] <= arr[-1] - x: arr.pop() else: arr.pop(0) return arr
方法2:堆
计算每个数字和x的距离,然后找出最近距离的数字。这就是常见的TopK问题。使用小根堆很容易实现。
时间复杂度是O(N),空间复杂度是O(N)。
class Solution(object): def findClosestElements(self, arr, k, x): """ :type arr: List[int] :type k: int :type x: int :rtype: List[int] """ N = len(arr) sub = [((arr[i] - x) ** 2, i) for i in range(N)] heapq.heapify(sub) return sorted([arr[heapq.heappop(sub)[1]] for i in range(k)])
方法3:二分查找
最简单思想的二分是找出x的位置,然后找出其旁边的k个数。但是下面这个做法惊为天人,比较区间两端的数值和x的距离,如果左边离得远了,就向右边走;如果右边离得远了,就向左边走。这个对二分查找的理解必须很深刻了。
class Solution(object): def findClosestElements(self, arr, k, x): """ :type arr: List[int] :type k: int :type x: int :rtype: List[int] """ left = 0 right = len(arr) - k while left < right: mid = int(left + (right - left) / 2 mid = int(mid) #记得mid是整数 if x - arr[mid] > arr[mid + k] - x: left = mid + 1 else: right = mid return arr[left : left + k] s = Solution() print(s.findClosestElements([1,2,3,4,5], 4, 3))
output:[1,2,3,4]
13、长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:
如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
方法1:双指针+滑动窗口
初试化左指针l=0。最小长度res=Max。初始化中间和tmp=0
遍历右指针,遍历区间[0,n),对于r:
记录累计和,tmp+=nums[r]。
循环条件tmp>=s,进入循环:
记录此时的最小长度,res=min(res,r−l+1)
累计和减去左指针,并将左指针加一。tmp-=nums[l],l+=1,这两句的顺序不能改变!
若res==Max,返回0,否则返回res。
时间复杂度:O(n),进行了一次遍历。
空间复杂度:O(1)。
class Solution: def minSubArrayLen(self, s: int, nums: List[int]) -> int: n = len(nums) l = 0 res = float("inf") tmp = 0 for r in range(n): tmp += nums[r] while(tmp >= s): res = min(res,r-l+1) tmp -= nums[l] l += 1 return res if(res != float("inf")) else 0
注意:
Python中可以用如下方式表示正负无穷:
float("inf"), float("-inf")
14、有序矩阵中第k小的元素
给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素。
请注意,它是排序后的第k小元素,而不是第k个不同元素。
示例:
matrix = [
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
],
k = 8,
返回 13。
说明:
你可以假设 k 的值永远是有效的, 1 ≤ k ≤ n2 。
分析:
二分查找法,根据题目可得左上角元素最小,右下角元素最大,计算中间值。然后计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找,类似于题目“二维数组的查找”
时间复杂度:O(nlogk) ,k=最大值-最小值。
class Solution(object): def kthSmallest(self, matrix, k): """ :type matrix: List[List[int]] :type k: int :rtype: int """ # 计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找 def count_num(m, target): i = 0 j = len(m) - 1 ans = 0 while i < len(m) and j >= 0: if m[i][j] <= target: ans += j + 1 i += 1 else: j -= 1 return ans # 思路:左上角元素最小,右下角元素最大,计算小于等于中间值的元素个数 left = matrix[0][0] right = matrix[-1][-1] # 二分法查找 while left < right: mid = (left + right) >> 1 # print(' mid = ', mid) count = count_num(matrix, mid) # print('count = ', count) if count < k: left = mid + 1 else: right = mid return left
s = Solution() matrix = [[1,5,9], [10,11,13], [12, 13, 15]] print(s.kthSmallest(matrix, 8))
output :13