继续来更新LeetCode的学习,本Part对应左神视频027-031。
感受算法之美吧~
一、堆结构常见问题
题目1:合并K个有序链表。
题目的输入是一个List,里面每个元素都是链表的头,有可能存在空,并且每个链表都是升序排列。我们的昨天思路就很简单了,用小根堆实现吗,一下子就清晰了,我们构建的小根堆肯定最根部的节点是最小的。首先将list中的头节点组成小根堆,之后只要堆不空,我们弹出根节点,第一次弹出的根节点就是最后输出的头节点。弹出根节点后,将该节点的下一个节点压入堆的0位置(前提是该节点有下一个节点),再heapify排序,再弹出根节点,并且让输出的头节点指向该根节点,如此往复即可。在python实现时,我们堆内的数据都是节点是无法直接比较大小的,所以我们需要定义一个比较器,比较节点的值进行排序而不是节点。
我们使用了python的heapq库进行实现,heappush会自动按照我们规定的__lt__规则实现排序,heappop会在弹出后继续使得该堆维持小根堆。主要注意该lambda表达式即可。
有关heapq库可以看这个博客Python heapq库的用法介绍-优快云博客
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
ListNode.__lt__ = lambda a, b: a.val < b.val # 让堆可以比较节点大小
import heapq
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
h = []
for l in lists:
if l:
heapq.heappush(h,l)
if not h:
return None
head = heapq.heappop(h)
pre = head
if pre.next:
heapq.heappush(h, pre.next)
while len(h)!=0:
cur = heapq.heappop(h)
pre.next = cur
pre = cur
if cur.next:
heapq.heappush(h, cur.next)
return head
题目2:线段最多重合问题。
在leetcode中为会议室问题,但是需要会员QAQ本人还不是会员就没有做这个题。。。。。就只能放一下思路了。每个线段都有一个起点一个终点,如果两个线段有重合,那么一定以某个线段的左边为边界,如果一个线段的结束位置都比另一个线段的开始位置小,那这两个线段不可能重合。所以思路如下,首先根据线段的开始位置排序线段,之后组织一个小根堆,该小根堆里放的是排序之后线段的结束位置。比如排序之后的线段为(1,5) (1,3) (2.6) (3,7) (5,9),我们首先压入(1,5)的5(默认压入就会自动排序为小根堆,可以自己写函数也可以用自带的库),此时堆不空,自己和自己是重合的所以有一个线段和自己重合,之后(1,3)准备进入,我们先将堆内所有<=准备进入元素的起始位置的元素弹出,再将(1,3)压入到小根堆。其实就是如果一个线段的结束位置都比另一个线段的开始位置小,那这两个线段不可能重合这句话的体现,如果你堆内的元素(堆内元素是结尾位置),都比我要进入元素的起始位置小说明不可能重合,我就弹出呗,之后我再统计堆里的元素个数不就是所有和进入线段有重合的线段的总数了嘛。当我们压入(1,5) (1,3) (2.6),再准备压入(3,7)时,(1,3)就要被弹出了,因为3<=3,(1,3)(3,7)就不会重合。最后保留该过程中最大的栈内元素个数就是输出。我这里贴出gpt转化的代码。
import heapq
class Solution:
def minMeetingRooms(self, meetings):
n = len(meetings)
# 按照会议的开始时间进行排序
meetings.sort(key=lambda x: x[0])
# 优先队列(小根堆),用于存储会议的结束时间
heap = []
ans = 0
for i in range(n):
# 如果堆顶的会议结束时间小于等于当前会议的开始时间,则移除堆顶
while heap and heap[0] <= meetings[i][0]:
heapq.heappop(heap)
# 把当前会议的结束时间加入堆
heapq.heappush(heap, meetings[i][1])
# 记录所需会议室的最大数量
ans = max(ans, len(heap))
return ans
题目3 让整个数组累加和减半的最少操作次数。Leetcode要求是至少减少恰好减半的最少操作次数,感觉这个要求对python并没有太多限制,因为python不像C++ Java那样要定义数据类型,有小数只要自己不限制为int类型,会自动转化为float🤣,也不要考虑溢出啥的。那其实思路就很简单了,将所有数排列成大根堆,取根元素(肯定是最大的),除2,再将根元素除2之后的数替换为根元素,重新排序,就这样一直除,直到满足条件统计次数即可。值得注意的点是,自己自下而上建堆更快,我们将每位乘2^20次方再做除法可以保证肯定是整形就不用变成float或double类型了。
class Solution:
def halveArray(self, nums: List[int]) -> int:
sum = 0
size = len(nums)
discount = 0
count= 0
self.num = nums[:]
for i in range(size-1,-1,-1):
self.num[i] = self.num[i]
sum += self.num[i]
self.heapify(i,size)
while discount<sum/2:
self.num[0] = self.num[0]/2
discount += self.num[0]
count+=1
self.heapify(0,size)
return count
def heapify(self,i,size):
l = 2*i+1
while l<size:
r = l+1
if r<size and self.num[r] > self.num[l]:
best = r
else:
best = l
if self.num[i] < self.num[best]:
self.swap(i,best)
i=best
l = 2*best+1
else:
break
def swap(self,i,j):
temp = self.num[i]
self.num[i] = self.num[j]
self.num[j] = temp
二、基数排序
基数排序和计数排序都是不基于比较的排序,之前学习的排序方法都是基于比较的排序。基于比较的排序比较通用,我们定义一个如何比较两个数的标注即可。不基于比较的排序依赖对象的数据特征,看完这节也就懂了。
计数排序:首先要求样本是整数并且范围要小,比如我这个数组里所有的数都是[0,60]之间,那么我们只用统计这61个数字每个数字出现的频率即可排序。如果是小数就没办法统计了,范围过大也不好统计。
基数排序:要求样本是非负整数,如果有负数也可以处理,我们取数组的最小值,给每个数都减去该最小值,那么此时肯定所有数都非负,再排序,最后再加上该最小值即可。基数排序的思想就是,先确定最大数有几位,从个位开始比较,先按照个位的顺序压入各个桶中(先进先出),对于10进制就有10个桶分别代表0~9,压入之后依次倒出。之后再重复各个位即可。举例说明[23,51,42,24,6],按照个位排序就是[51,42,23,24,6],之后按照十位排序桶(0)->[6],桶(2)->[23,24](23先进入就必须23先出),桶(4)->[42],桶(5)->[51],依次倒出排序[6,23,24,42,51]。先进先出保证了该位大小一样是遵循次一位的顺序,保证有序。
实现中有两个技巧:1.前缀分区技巧,就是我们在构造桶是将桶(x)中有几个元素转化为<=x一共有几个元素,改变桶内存放的东西,不在存放具体的数字而是存放小于等于该桶对应数字的个数。比如说桶(3)=7,就代表该位<=3的元素一共有7个,也就是说该位为3的元素应该从数组下标[6]开始向左放入,放入之后该位<=3的元素个数就应该-1。2.如何取该位的数字,定义offset=1,取个位就是(num/offset)%10,之后offset*=10去取个位即可。
还有一个就是python中确定最大数有几位,我是将其转换成字符串在统计字符串个数。该代码考虑了存在负数的情况,不然会报错。。。。
class Solution:
def __init__(self):
self.base = 10
def sortArray(self, nums: List[int]) -> List[int]:
min_num = min(nums)
for i in range(len(nums)):
nums[i] -= min_num
max_num = max(nums)
bits = len(str(max_num))
result = self.RadixSort(nums,bits,self.base)
for i in range(len(nums)):
result[i] += min_num
return result
def RadixSort(self,arr,bits,base):
offset = 1
result = [0]*len(arr)
for i in range(bits):
cnts = [0]*base
for j in range(len(arr)):
index = int((arr[j]/offset)%base)
cnts[index] +=1
for j in range(self.base-1):
cnts[j+1] = cnts[j] + cnts[j+1]
for j in range(len(arr)-1,-1,-1):
index = int((arr[j]/offset)%base)
position = cnts[index]
result[position-1] = arr[j]
cnts[index] -=1
for j in range(len(arr)):
arr[j] = result[j]
offset *=10
return arr
三、排序算法总结
就是这张图了,总结了学过的所有排序,选择、冒泡、插入、归并、随机快速、堆、计数、基数排序。稳定性就是在排序过程中是否会改变原本数组中数字的相对顺序。稳定性对基础类型对象来说毫无意义;稳定性对非基础类型对象有意义,可以保留之前的相对次序
基于比较的排序,时间复杂度O(n*logn),空间复杂度低于O(n),还具有稳定性的排序算法目前没有找到
排序的选择指标:
1.数据量非常小的情况下可以做到非常迅速:插入排序
2.性能优异、实现简单且利于改进(面对不同业务可以选择不同划分策略)、不在乎稳定性:随机快排
3.性能优异、不在乎额外空间占用、具有稳定性:归并排序
4.性能优异、额外空间占用要求O(1)、不在乎稳定性:堆排序
下面两个位运算的骚操作实现真的十分惊艳
四、异或运算的骚操作
首先说明,python会自动升位不存在溢出,python3中就没有long这个类型了。并且python对于负数的转化也有点区别,后面会说。
异或运算的性质:什么是异或运算,一句话不进位的加法,十分巧妙的总结了相同为0相异为1。
异或运算满足交换律和结合律,0^n=n n^n=0
整体异或和如果是x,整体中某个部分的异或和为y,那么剩余部分的异或和就是x^y。(题目最多)
1.交换两个数。
a,b交换->a=a^b b=a^b a=a^b,三行代码完成交换。从第二步开始看,此时a=a^b带入第二个式子,b=a^b^b=a了,在带入第三个式子此时b=a,a=b^b=b了。注意如果a=b那么第一步就会将a变成0,此时无法完成交换。
2.不用任何比较语句返回两个数的最大值。
逻辑就是不用比较那就是只能用符号位来判断,比较ab大小,首先定义c=a-b此时c有可能溢出,我们首先取a,b,c的符号位,如果是非负则为1,负数为0记为sa,sb,sc,首先判断ab的符号是否一样,相同为0,不同为1,记为diffAB,sameAB = ~diffAB。此时如果要返回a,也就是a比b大,有两种情况,1).ab符号不同并且a是非负的,2).ab符号相同并且此时sc为非负的,所以returnA=diffAB*sa+sameAB*sc,returnB就是returnA的取反。最终返回returnA*a+returnB*b即可。
class Solution:
# 必须保证n一定是0或者1
# 0变1,1变0
def flip(self, n):
return n ^ 1
# 非负数返回1,负数返回0
def sign(self, n):
return self.flip(n >> 31 & 1) # 使用右移和按位与处理符号位
# 有溢出风险的实现
def getMax1(self, a, b):
c = a - b
returnA = self.sign(c)
returnB = self.flip(returnA)
return a * returnA + b * returnB
# 没有任何问题的实现
def getMax2(self, a, b):
c = a - b
sa = self.sign(a)
sb = self.sign(b)
sc = self.sign(c)
diffAB = sa ^ sb
sameAB = self.flip(diffAB)
returnA = diffAB * sa + sameAB * sc
returnB = self.flip(returnA)
return a * returnA + b * returnB
3. 找到缺失的数字。
比如是0-10中缺少了9,组成了一个长度为10的数组,那么我们将0-10全部异或,在将数组中的数字全部异或,这两个异或值再异或就是丢失的数了。
整体异或和如果是x,整体中某个部分的异或和为y,那么剩余部分的异或和就是x^y
class Solution:
def missingNumber(self, nums: List[int]) -> int:
xor_all = 0
xor_num = 0
for i in range(len(nums)):
xor_all ^= i
xor_num ^= nums[i]
xor_all ^= len(nums)
return xor_all^xor_num
4.数组中1种数字出现奇数次,其余的都出现偶数次,返回出现奇数次的数。
出现偶数次的数异或完就是0了,出现奇数次的数异或完就是自己。
class Solution:
def singleNumber(self, nums: List[int]) -> int:
xor = 0
for i in range(len(nums)):
xor^=nums[i]
return xor
如何找到一个数在二进制下最右边的那个1所在的状态?
比如:n=0b01101000,我们希望找到1000这个状态。
Brain Kernighan算法:该状态为n&(-n) 直接背过吧。。。
5.给你一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。. - 力扣(LeetCode)
比如ab出现了一次,其余都出现了两次,那么我们遍历数组求异或和最终的结果就是a^b,a^b一定有至少一位不同导致该位为1,那么我们用Brain Kernighan算法找到a^b最右边的1的状态,该位一定存在,假设我们找到了第3位是1。此时该数组可以分成两部分,第三位是1的和第三位是0的,并且ab一定一边一个,那么此时我们遍历数组,将第三位是1的都做异或运算,一定能得到a或者b其中一个,再与最开始得到的a^b结果做异或操作得到另一个数。
class Solution:
def singleNumber(self, nums: List[int]) -> List[int]:
xor1 = 0
for i in range(len(nums)):
xor1 ^= nums[i]
right_one = xor1&(-xor1)
xor2 = 0
for i in range(len(nums)):
if nums[i] & right_one ==0:
xor2 ^= nums[i]
return xor2,xor1^xor2
6.给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。. - 力扣(LeetCode)
思路十分巧妙我们统计数组中每个元素每一位上的元素之和,最终如果该位能被3整除那么仅出现一次的元素该位一定是0否则是1.
举例[0110,0001,1101,1110],比如前三个元素都出现了3次只有最后一个元素出现了1次。我们准备一个数组长度为4,数组每个位置保存该位之和,cnt[3] = 0*3+0*3+1*3+1*1=4%3=1,所有出现一次的元素第3位是1,cnt[0] = 0*3+1*3+1*3+0*1=6%3=0 所以出现一次的元素0位是0.
需要注意的是python并不会第31位是1输出负数,因为python没有位数概念会自动升位,所以如果最高位是1,说明是负数,需要用前30位的结果减去1<<31得到对应的负数,或者将此反码转化为原码让python输出。
class Solution:
def singleNumber(self, nums: List[int]) -> int:
cnts=[int(0)]*32
for i in range(len(nums)):
for j in range(32):
cnts[j] += (nums[i]>>j)&1
ans = 0
for j in range(32):
if cnts[j]%3 !=0:
if j==31:
pass
ans -= 1<<j #减去该值 如果是负数了话比如-4,此时除去第31位,其余数代表的状态为2147483644(1111111111111111111111111111100)
# ans |= 1<<j #python 没有位数概念
# ans = ~(ans^0xFFFFFFFF) #将这个反码数,转换为原码让Python输出
else:
ans |= 1<<j
return ans
如果说异或运算的还算是有可能想到,那么位运算中的几个方法真的只能说惊为天人了。。。。
五、位运算的骚操作
1. 判断一个数是不是2的幂。
简单,2的幂肯定只有一位是1,其余都是0,我们用Brain Kernighan算法找到最右侧的1的状态,如果和该数相等那就是2的幂。这个数肯定得>0否则不可能是2的幂
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
return n>0 and (n&(-n)) == n
2.判断一个数是不是3的幂。
这个感觉和位操作关系不大啊😂32位数中3的幂最大为3**19,并且3又是质数,所以3的幂肯定可以被3**19整除,而且这个数必须大于0.
class Solution:
def isPowerOfThree(self, n: int) -> bool:
return n>0 and 3**19%n==0
3.返回大于等于n的最小2的幂。
首先n--,因为如果不n--后续的操作会让n本身就是2的幂时无法返回自身。n--之后,我们让最左侧的1后面的所有位都变成1,再n++,是不是就返回了题目要求的2的幂。如何实现呢?直接看代码,再举例说明。
比如n:00100100,先n = n-1此时n:00100011,先让n右移1位再与n或就是 00100011 | 00010001,此时最左侧的1和最左侧1的下一位一定都是1,此时n=00110011,n右移2位再与n或就是 00110011 | 00001100 此时left_1与left_1+1,left_1+2,left_1+3这四位都肯定是1,n=001111111,之后再移动四位,就可以保证,最开始的最左侧的1之后的位都变成1,32位数同理。
class Solution:
@staticmethod
def near2power(n):
if n <= 0:
return 1
n -= 1
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
return n + 1
# 示例运行
if __name__ == "__main__":
number = 100
print(Solution.near2power(number))
4.区间[left right]内所有数字&的结果。
看代码一头雾水,啥玩意啊,为啥一行就能写完还不是遍历。思路是,我们要的是与的结果,与操作就是只要存在0肯定是0,这一位必须有1才有可能与完是1,我们题目是区间内的所有数字,那么意味着,我最多只能保留最右边数字本身存在的所有1(left == right的情况)。所以,我只用每次将right最右边的1变成0,如果此时小于了left,此时的right就是所有与的结果,因为其余有1的位肯定是相同的。
举例: right = 0101001101,先去掉最右边的1变成 0101001100,如果此时小于等于left了,是不是此时所有的1都能保留下来,毕竟此时只是减1,那只能说left就是这个数了。如果还是比left大,再将最右边的1变成0,就是减去这个状态变成0101001000,如果此时小于left了,比如left = 0101001001,此时,left-right就是0101001001-0101001101,我们看0,1,2这三位,在这个区间内必然有该位等于0的情况,那么这三位与完只能都是0,所以只能保留住0101001000该状态。代码就是实现了这个逻辑。
class Solution:
def rangeBitwiseAnd(self, left: int, right: int) -> int:
while left < right:
right -= right&(-right)
return right
5. 反转一个二进制状态。
类似于将二进制看成一个数组,反向输出这样。不是二进制取反😂。就是将00111010->01011100这样。
思路:以8位为例,假设这8位是abcdefgh,首先我们1v1交换,就是ab交换,cd交换,ef交换,gh交换->badefehg,再2v2交换,ba和dc交换,fe和hg交换->dcbahgfe,再4v4交换,dcba与hgfe交换->hgfedcba,完成。那么如何交换呢?位操作出场对于1v1交换,就是abcdefg&10101010->a0c0e0g0 abcdefg&01010101->0b0d0f0h,再让a0c0e0g0右移一位,0b0d0f0h左移1位,进行或操作是不是就是0a0c0e0g | b0d0f0h0 就变成了badcfehg是不是就完成了1v1。继续2v2,badcfehg&11001100->ba00fe00,badcfehg&00110011->00dc00hg,再让ba00fe00右移两位,00dec00hg左移两位,进行或操作 00ba00fe | dc00hg00->dcbahgfe。继续4v4,dcbahgfe&11110000->dcba0000,dcbahgfe&00001111->0000hgfe,dcba0000右移四位 | 0000hgfe左移四位,得到hgfedcba。扩展到32位同理。
注意括号使用保证运算优先级!
class Solution:
def reverseBits(self, n: int) -> int:
n = ((n&(0xaaaaaaaa))>>1) | ((n&0x55555555)<<1)&0xffffffff
n = ((n&(0xcccccccc))>>2) | ((n&0x33333333)<<2)&0xffffffff
n = ((n&(0xf0f0f0f0))>>4) | ((n&0x0f0f0f0f)<<4)&0xffffffff
n = ((n&(0xff00ff00))>>8) | ((n&0x00ff00ff)<<8)&0xffffffff
n = (n>>16) | ((n<<16)&0xffffffff)
return n
6.返回一个数二进制中有几个1。
leetcode上的题是计算汉明距离,就是这两个数字对应二进制位不同的位置的数目,我们先将这两个数异或一下统计里面的1的个数就行了。
思路:我们一步一步计算长度为2,4,8,16,32时该二进制上每段1的个数,最开始的数字就是长度为1时,每段上是1就是有一个1是0就有0个1,长度为2时如果该段为01就是一个1,10就是2个1,00就是0个1,一次类推。
举例说明:以8位为例,11111001,首先11111001&01010101->01010001,再让11111001右移1位也&01010001->01010100,我们将01010001+01010100 = 10100101,该数字两位两位看,10表示第7和6位有2个1,下一个10表示第5和4位有两个1,下一个01表示第3和2位有1个1,最后的01表示第1和0位一共有1个1.继续操作,10100101&00110011->00100001,再让0100101右移2位也&00110011->00100001,之后00100001+00100001= 01000010,我们4位4位看,0100表示高4位上有4个1,0100对应的十进制就是4,第四位0010表示第四位上有2个1。再进行一次,01000010&00001111->00000010, 01000010右移四位再&00001111->00000100,00000010+00000100=00000110,表示在8位上一共有4+2=6个1,计算完毕。
自己想肯定时想不出来的😂,逻辑就是不断的让每段含有的1的数量相加。
class Solution:
def hammingDistance(self, x: int, y: int) -> int:
n = x^y
n = (n & 0x55555555) + ((n >> 1) & 0x55555555)
n = (n & 0x33333333) + ((n >> 2) & 0x33333333)
n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f)
n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff)
n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff)
return n
条件判断相比于赋值、位运算、算术运算是稍慢的,大牛的实现欣赏完理解就好,下次当模版直接用。大牛的炫技是真的酷炫啊~