很久没有写Leetcode刷题的相关博客了,一来最近比较懒,二来刷的那些题目都不难,没有给我什么新的知识点,就没有写。今天碰到了一个位操作的题,以前这类题目我是不会去碰的,因为不熟悉,但是终究是要接触的。这道题一开始我并没有写出来,提交结果超时,在参考了其他人的解法思路后最终通过。
题目如下:
Given a non-empty array of numbers, a0, a1, a2, … , an-1, where 0 ≤a i< 231.
Find the maximum result of a i XOR a j , where 0 ≤ i, j < n.
Could you do this in O(n) runtime?
Example:
1 | Input: [3, 10, 5, 25, 2, 8] |
题目大意
给定一个非空数组,里面所有数字属于区间(0, 231)。找出这个数组中两两异或的最大值。要求时间复杂度为O(n)
思路
naive solution
最简单的方法当属暴力搜索,两层循环,记录最大的异或值即可,代码比较简单,但是会超时。
1 | public int findMaximumXOR(int[] nums) { |
此种解法时间复杂度为O(n2),空间复杂度为O(1)
better solution
我们还需要用上一个异或的特性,假设a
和b
产生了最终的答案max
,即a ^ b = x
,那么根据异或的特性,a ^ x = b
。同理,a
和b
的最高位(前n
位)也有相同的性质。
先以最高位为例子,我们可以把所有的数字的最高位放到一个HashSet
里面,然后使用1
与set
里面的所有数字进行异或,如果得出的结果仍然在set
里面,那么最终结果的最高位必然为1
,否则为0
。也即,先假定结果为1
,然后与set
中所有数字异或,假定a
与1
异或得到结果b
(a ^ 1 = b
),而b
仍然在set
里面,那么说明set
中有两个数字异或能得到1
(a ^ b = 1
)。否则,set
中没有两个数字能够异或得到1
,那么最终结果的最高位为1
的假设失败,说明最终结果的最高位为0
。以此类推可以得到第二位、第三位。。。的数字。
再做一下推广,我们将所有数字的前N位放到一个HashSet
里面,然后使用之前N-1
位得到的最大值前缀prefix
与set
里面的所有数字进行异或,如果得出的结果仍然在set
中,那么第N位必然为1
,否则为0
。
举个例子,给定数组[14, 11, 7, 2]
,二进制表示分别为[1110, 1011, 0111, 0010]
。题目说了,数字最长不会超过32
位,所以应从i = 31
开始,但是所有数字中最多位4位数,简单起见,我直接从最高位i=3
开始
1 | 1. i = 3, set = {1000, 0000} => max = 1000 |
最终答案是1100 => 12
,1011 ^ 0111 = 1100(11 ^ 7 = 12)
。
Java
版代码如下
1 | public int findMaximumXOR(int[] nums) { |
此解法时间复杂度为O(32n)=O(n)
,空间复杂度上,我们使用了一个HashSet
用于存储所有数字,因此空间复杂度是O(n)
。和暴力搜索解法相比,典型的空间换时间。附上AC时间图
anothor better solution
我以为,上面的解法已经是最佳解法了,结果却发现前面还有一个提交时间小山峰,查看了一下发现有更好的解法,此解法用到了Tries
的数据结构。此数据结构我从来之前都没听说过,研究了一下算法后发现,其实Tries
类似于二叉树,主要的思路是这样:构建一棵深度为33
的二叉树。root
节点左孩子为1
,右孩子为0
代表着所有数字的最高位,其次根据次高位继续往下。如果某一个节点左右子树都不为空,那么得到最终答案的两个数字肯定分别出自于左右子树且此位为1
;如果任意一个为空,那么最终答案该位为0
,依次迭代得到最终结果。对此解法我并没有详细研究,感兴趣的可以自己去看看
Java O(n) solution using Trie
cheat solution
是的,通过查看时间表我发现了一个作弊的答案。
此题一共29
个测试用例,最后一个测试用例是用来测试时间复杂度的,长度是20000。超时的原因就是因为这最后一个测试用例,因此有答案直接对此做了特殊处理,然后使用暴力解法……最后只用时15ms
1 | public int findMaximumXOR(int[] nums) { |
可怕的人类!