LeetCode 217 题解
217 - Duplicate Nums,是一道求解列表里是否有重复数字的题目
乍一看用哈希 或者排序算法 都可以解决,是个非常简单的题目,但是在实际的速度上都没有达到最优,因此记录一下优化的过程
1. Hash
哈希算法的时间复杂度为O ( N ) O(N) O ( N ) ,空间复杂度也为O ( N ) O(N) O ( N ) ,第一遍简单用unordered_map写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bool containsDuplicate (vector<int >& nums) { unordered_set<int > s; for (int x : nums) { if (s.count (x)) return true ; s.insert (x); } return false ; } bool containsDuplicate (vector<int >& nums) { unordered_set<int > s; s.reserve (nums.size ()); for (int x : nums) { if (!s.insert (x).second) { return true ; } } return false ; }
甚至如果不从中途退出,直接比较最终的结果的话,牺牲一点时间就能得到很"炫炮"的一行代码
1 2 3 bool containDuplicate (vector<int >& nums) { return unordered_set <int >(nums.begin (),nums.end ()).size () < nums.size (); }
直接利用cpp提供的unordered_map来写,虽然直观,但是并没有得到很好的结果,其原因是unordered_map的底层是由拉链法实现的。它的组成部分为:
桶数组(Bucket Array) :一段连续的内存空间,存储指向链表的指针
链表节点(Linked Nodes) :存储实际元素的节点,通过指针连接
因为是动态分配 ,所以它存在在堆 上,可能占用的是不连续的内存,遍历的时候需要跳跃访存 ;同时当新元素出现的时候,需要进行一个扩容 (rehash) 。而扩容的过程是重新分配一个更大的桶数组(和用数组实现堆/栈相似),并且释放旧的桶数组并重新组织链表。由此可以小小优化一下
1 2 3 4 5 6 7 8 9 10 bool containsDuplicate (vector<int >& nums) { unordered_set<int > s; s.reserve (nums.size ()); for (int x : nums) { if (s.count (x)) return true ; s.insert (x); } return false ; }
2. Sort
先排序,之后查看相邻的元素是否相同即可
理论上来说根据不同的排序方法,时间复杂度为O ( N + N ) O(N +N) O ( N + N ) 到O ( N log N + N ) O(N\log N + N) O ( N log N + N ) 不等,比使用hash 要更慢,但是实际上测试下来都比unordered_map要慢,
核心因素可能是数组在内存中连续,CPU的L1/L2 Cache 对于这种情况读取效率高
1 2 3 4 5 6 7 8 bool containsDuplicate (vector<int >& nums) { sort (nums.begin (), nums.end ()); for (int i = 0 ; i < nums.size () - 1 ; ++i) { if (nums[i] == nums[i + 1 ]) return true ; } return false ; }
发现排序更有效之后,再试试用一些非比较排序 的方法来优化,比如基数排序 (牺牲空间)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #pragma GCC optimize("Ofast,unroll-loops" ) #pragma GCC target("avx2,bmi,bmi2,lzcnt,popcnt" ) static const int speedup = []() { std::ios_base::sync_with_stdio (false ); std::cin.tie (NULL ); return 0 ; }(); class Solution {public : bool containsDuplicate (vector<int >& nums) { int n = nums.size (); if (n <= 1 ) return false ; vector<int > buffer (n) ; int * src = nums.data (); int * dst = buffer.data (); int count[65536 ]; for (int i = 0 ; i < 65536 ; ++i) count[i] = 0 ; for (int i = 0 ; i < n; ++i) { count[(unsigned short )(src[i] ^ 0x80000000 )]++; } for (int i = 1 ; i < 65536 ; ++i) count[i] += count[i - 1 ]; for (int i = n - 1 ; i >= 0 ; --i) { dst[--count[(unsigned short )(src[i] ^ 0x80000000 )]] = src[i]; } swap (src, dst); for (int i = 0 ; i < 65536 ; ++i) count[i] = 0 ; for (int i = 0 ; i < n; ++i) { count[(unsigned int )((unsigned int )(src[i] ^ 0x80000000 ) >> 16 )]++; } for (int i = 1 ; i < 65536 ; ++i) count[i] += count[i - 1 ]; for (int i = n - 1 ; i >= 0 ; --i) { dst[--count[(unsigned int )((unsigned int )(src[i] ^ 0x80000000 ) >> 16 )]] = src[i]; } for (int i = 0 ; i < n - 1 ; ++i) { if (dst[i] == dst[i + 1 ]) return true ; } return false ; } };
这是目前为止最快的版本,但是还是没有达到 leetcode 排行榜中的最快
3. Customized HASH
基数排序分为两部分进行了处理,并且每一步都包含了初始化 和搬运 这两个小循环,而 hash 理论上只要搬运一次数据。因此看看有没有更好的 hash 实现方式
之前使用的unordered_map的数据结构是有利于完成之后的查询的一种结构,但是这道题其实只需要存储 这一步,因为 hash 的特性会在存储的时候计算出映射,所以这个时候就已经能检验是否重复了。这里尝试了一个简单的线性探测,因为不会涉及到取出这个元素,所以它本身的主聚类 导致查找的性能损失的劣势不复存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : bool containsDuplicate (vector<int >& nums) { int n = nums.size (); if (n < 2 ) return false ; int hashSize = 1 ; while (hashSize < n * 2 ) hashSize <<= 1 ; int mask = hashSize - 1 ; vector<int > hashTable (hashSize, INT_MIN) ; for (int x : nums) { unsigned int h = (unsigned int )x; int idx = h & mask; while (hashTable[idx] != INT_MIN) { if (hashTable[idx] == x) return true ; idx = (idx + 1 ) & mask; } hashTable[idx] = x; } return false ; } };
几个细节:
hash表的大小:要是负载因子大于 0.5,就要开辟大于 2*n 的空间,同时要保证 hashsize 为2的幂次,便于后面 计算hash 的优化
计算hash:线性探测需要对数字和 hashsize 进行一个取模的运算。而 mod 运算处理起来是一个除法,需要复杂的逻辑单元处理;而按位与&只是简单的位运算,在汇编的时候只是一个 AND指令,运算代价小
总结
最终线性探测 方法胜出
另:数据结构的一些算法已经快忘光了