力扣刷题日记

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

示例 1:
输入:nums = [2,2,3,2]
输出:3

示例 2:
输入:nums = [0,1,0,1,0,1,100]
输出:100

提示:
1 <= nums.length <= 3 * 104
-231 <= nums[i] <= 231 - 1
nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

思路1:哈希表

第一想法:哈希表。两次遍历:第一次遍历用哈希表存储每个数字出现的次数;第二次遍历找出只出现一次的数字。
C++代码如下:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        unordered_map<int, int> um;
        int size = nums.size();
        for(int i = 0; i < size; ++i){
            um[nums[i]]++;
        }
        for(int i = 0; i < size; ++i){
            if(um[nums[i]] == 1)
                return nums[i];
        }
        return 0;
    }
};

时间复杂度:O(n);空间复杂度:O(n)。
在这里插入图片描述

思路2:排序

排序后进行遍历,C++代码如下:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int size = nums.size();
        for(int i = 0; i < size - 1;){
            if(nums[i] == nums[i + 1])
                i += 3;
            else
                return nums[i];
        }
        return nums[size - 1];
    }
};

在这里插入图片描述
这里偷懒,使用了STL的排序函数,具体的排序算法我之前的博客有写过(快排,归并,计数等),这里不再重复了,附上链接:
专栏地址: 常见排序算法

思路3:遍历统计每一个二进制位,结合位运算

对于数组中的非答案元素,每个都出现了 3 次,那么相对应的二进制位上的 0 或 1 也都出现了 3 次。对3进行取余后为 0,就消除掉了非答案元素的影响。遍历统计每一个二进制位,然后结果对3进行取余,所得结果即为最终的答案,即只出现一次的元素。

代码如下:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
    	int ret = 0, total = 0;
    	for(int i = 0; i <32; ++i){
    		total = 0;
    		for(int x : nums)
    			total += (x >> i) & 1;
    		if(total % 3)
    			ret += (1 << i);
    	}
    	return ret;
    }
};

时间复杂度:O(n*32);空间复杂度:O(1)。
在这里插入图片描述
扩展
数组中只有一个数字出现 m 次,其余都出现 n 次,找出出现 m 次的数字(假设 m 不能被 n 整除)。

可以对上述方法进行扩展,数组中所有数字的对应二进制位上 1 或 0 的总数可以被 n 整除。每个二进制位的 0 或 1 的个数对 n 进行取余,所得结果即为出现 m 次的数字。

哈希表

哈希表(Hash table,也叫散列表),是根据键值对(Key,value)而直接进行访问的数据结构。也就是说,它通过把键值对映射到表中一个位置来访问记录(即,记录的存储位置= H(关键字)),以加快查找的速度。这个映射函数叫做散列函数(哈希函数),使用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表(哈希表)。哈希表本质上是一个数组。

哈希表hash table(key,value) 就是把Key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。)

而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

哈希函数(核心)构造方法

(1)直接定址法:
哈希函数为关键字的线性函数如 H(key)=a*key + b。
这种构造方法比较简便,均匀;但是仅限于地址大小=关键字集合的情况。
(2)数字分析法:
假设关键字集合中的每个关键字key都是由s位数字组成(k1, k2, …, kn),分析key中的全体数据,并从中提取分布均匀的若干位或他们的组合构成全体。
此种方法通常用于数字位数较长的情况,必须数字存在一定规律,其必须知道数字的分布情况。比如事先知道这个班级的学生出生在同一年,同一个地区。那么他们的身份证的前面数位都是相同的,那么我们可以截取后面不同的几位存储。H(key)= key%100000。
(3)平方取中法:
如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址。
这种方法适合事先不知道数据并且数据长度较小的情况。
(4)折叠法:
如果数字的位数很多,可以将数字分割为几个部分,取他们的叠加和作为hash地址。如key=123 456 789,三部分和61524,取末三位,存在524的位置。
该方法适用于数字位数较多且事先不知道数据分布的情况。
(5)除留余数法,用的较多
H(key)=key MOD p (p<=m,m为表长),如何选取p是关键问题。p应为不大于m的质数或是不含20以下的质因子的合数,这样可以减少地址的重复(冲突)。
(6)随机数法:
H(key)= Random(key),取关键字的随机函数值为它的散列地址。

hash函数设计的考虑因素

计算散列地址所需要的时间(即hash函数本身不要太复杂);关键字的长度;表长;关键字分布是否均匀,是否有规律可循;设计的hash函数在满足以上条件的情况下尽量减少冲突。
实现哈希表可以采用两种方法:数组+链表;数组+二叉树。

哈希冲突

即不同key值产生相同的地址,H(key1)= H(keyi)。
不管hash函数设计的如何巧妙,总会有特殊的key导致hash冲突,特别是对动态查找表来说。hash函数解决冲突的方法有以下几个常用的方法:
开放定址法;链地址法;公共溢出区法(建立一个特殊存储空间,专门存放冲突的数据,适用于数据和冲突较少的情况);再散列法(准备若干个hash函数,如果第一个hash函数发生了冲突,使用第二个;第二个也冲突,使用第三个……)。

(1)开放定址法
如果H(key1)= H(keyi),那么keyi存储位置Hi = (H (keyi) + di) MOD m,m为表长。di有三种取法:
1)线性探测再散列:di = c ∗ i。
2)平方探测再散列:di = 12,-12,22,-22,…。表长m必须为4j+3的质数(平方探测表长有限制)。
3)随机探测再散列(双探测再散列):di是一组伪随机数列。随机探测时m和di没有公因子(随机探测di有限制)。如果还是冲突就继续加di。
增量di应该具有以下特点(完备性):产生的Hi(地址)均不相同,且所产生的s(m-1)个Hi能覆盖hash表中的所有地址。
19 01 23 14 55 68 11 86 37要存储在表长11的数组中,其中H(key)=key MOD 11。按照上面三种解决冲突的方法,存储结果如下:
线性探测再散列:55 1 23 14 68 11 37 [ ] 19 86 [ ]
平方探测再散列:55 1 23 14 37 [ ] 68 [ ] 19 86 11
随机探测再散列(取di=key MOD 10 +1):55 1 68 14 11 23 37 [ ] 19 86 [ ]

存在的问题以及解决方法:
开放寻址如果一直找不到空的位置怎么办?出现个情况的一个前提,那就是没有空位置了,但是实际情况是位置不会被占光的,因为有一定量的位置被占了的时候就会发生扩容。

当哈希表被占的位置比较多的时候,出现哈希冲突的概率也就变高了,所以很有必要进行扩容。

增长因子,也叫做负载因子,是已经被占的位置与总位置的一个百分比。若增长因子是0.75,那么达到了总位置的75%就需要扩容。而且这个扩容也不是简单的把数组扩大,而是新创建一个数组是原来的2倍,然后把原数组的所有键值对都重新Hash一遍放到新的数组。因为数组扩大了,所以哈希函数也会有变化,这里的Hash也就是把之前的数据通过新的哈希函数计算出新的位置来存放。

(2)链地址法
产生hash冲突后在存储数据后面加一个指针,指向后面冲突的数据。
在这里插入图片描述
存在的问题以及解决办法:
如果冲突过多的话,这块的链表会变得比较长。如果这里的链表长度大于等于8的话,链表就会转换成树结构,当然如果长度小于等于6的话,就会还原链表。以此来解决链表过长导致的性能问题。这样设计是因为中间有个7作为一个差值,来避免频繁的进行树和链表的转换,因为转换频繁影响性能。

哈希表的查找

查找、插入在大多数情况下可以达到O(1),时间主要花在计算hash上。
查找过程和造表过程一致,假设采用开放定址法处理冲突,则查找过程为:
对于给定的key,计算hash地址index = H(key);
如果数组arr【index】的值为空 则查找不成功;
如果数组arr【index】== key 则查找成功;
否则 使用冲突解决方法求下一个地址,直到arr【index】== key或者 arr【index】== null。

哈希表的查找效率

决定hash表查找的ASL因素:选用的hash函数;选用的处理冲突的方法;hash表的饱和度,装载因子α = n/m(n表示实际装载数据长度,m为表长,α=0.75是时间空间综合利用效率最高的情况)。假设hash函数是均匀的,则hash表的ASL是处理冲突方法和装载因子的函数。
线性探测再散列:Snl ≈ 0.5 * (1 + 1/(1 - α))
随机探测再散列:Snr ≈ -1/α * ln(1 - α)
链地址法:Snc ≈ 1 + α/2

哈希表的删除

首先链地址法是可以直接删除元素的,但是开放定址法是不行的,拿前面的双探测再散列来说,假如我们删除了元素1,将其位置置空,那 23就永远找不到了。正确做法应该是删除之后置入一个原来不存在的数据,比如-1。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值