前言
今天自己刷了三题都能用哈希法解决的算法题,算提前了解了哈希表的算法思想,很有收获。
一、第一个只出现一次的字符
题目:
在一个字符串中找到第一个只出现一次的字符, 并返回它的位置, 如果没有则返回 - 1(需要区分大小写).(从0开始计数)
数据范围:0≤n≤10000,且字符串只有字母组成。
要求:空间复杂度O(n),时间复杂度O(n)。
代码如下:
#include <stdio.h>
#include <string.h>
int FirstNotRepeatingChar(char* str)
{
size_t len = strlen(str);
char count[256] = { 0 };
int i = 0;
for (i = 0; i < len; i++)
{
count[str[i]]++;
}
for (i = 0; i < len; i++)
{
if (count[str[i]] == 1)
{
return i;
}
}
return -1;
}
int main()
{
char arr[] = "google";
int ret = FirstNotRepeatingChar(arr);
printf("%d\n", ret);
return 0;
}
这道题属于哈希表法的入门题,思路如下:
1、我们知道,在C 语言中,当将字符串作为形参传递时,实际上传递的是字符串首字符的地址。
在函数中,可以通过接收一个指向字符的指针来访问传递的字符串。这个指针指向字符串的第一个字符,因此可以通过指针访问整个字符串。
2、定义一个足够大的数组(char类型的取值为-128~127),无论是大小写字母,在其出现时都能找到其对应ASCLL值的位置上计数,第一次遍历数组记录每个字符出现的次数,第二次遍历数组就能找到计数为1,即只出现一次的数,第一次找到只出现一次数时直接返回位置即可。
再来看一种写法(主函数同上):
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int FirstNotRepeatingChar(char* str)
{
size_t len = strlen(str);
int* result = (int*)malloc(sizeof(int) * ('z' - 'A')); //包含所有的大小写字母
int i = 0;
for (i = 0; i < len; i++)
{
result[str[i] - 'A']++;
}
for (i = 0; i < len; i++)
{
if (result[str[i] - 'A'] == 1)
{
return i;
}
}
return -1;
}
这里我们动态内存分配了一个’z’-'A’的空间,保证足够存放所有大小写字母出现的次数,在计数时[str[i] - ‘A’]保证数组访问时是在开辟的空间上进行。
二、数组中出现次数超过一半的数字(含候选法)
题目:
给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如:输入一个长度为9的数组[1, 2, 3, 2, 2, 2, 5, 4, 2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。
数据范围:n≤50000,数组中元素的值0≤val≤10000
要求:空间复杂度:O(1),时间复杂度O(n)
1.哈希法
代码如下:
#include <stdio.h>
int MoreThanHalfNum_Solution(int* numbers, int numbersLen)
{
int count[10000] = { 0 };
int i = 0;
for (i = 0; i < numbersLen; i++)
{
count[numbers[i]]++;
}
for (i = 0; i < numbersLen; i++)
{
if (count[numbers[i]] > numbersLen / 2)
{
return numbers[i];
}
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,2,2,2,5,4,2 };
int len = sizeof(arr) / sizeof(arr[0]);
int ret = MoreThanHalfNum_Solution(arr, len);
printf("%d\n", ret);
return 0;
}
这段代码的思路和第一题一模一样,就不重复了,注意一下这里的空间复杂度:O(n),时间复杂度O(n),重点说一下下面的候选法。
2.候选法
代码如下(主函数同上):
int MoreThanHalfNum_Solution(int* numbers, int numbersLen)
{
int majority = numbers[0]; //假设第一个元素为候选众数
int count = 1; //候选众数的计数器
for (int i = 1; i < numbersLen; i++)
{
if (numbers[i] == majority)
{
count++;
}
else
{
count--;
}
if (count == 0) //如果计数器变为0,更新候选众数为当前元素
{
majority = numbers[i];
count = 1;
}
}
//验证候选众数是否为真正的众数
count = 0;
for (int i = 0; i < numbersLen; i++)
{
if (numbers[i] == majority)
{
count++;
}
}
if (count > numbersLen / 2)
{
return majority;
}
return -1;
}
思路:
在这段代码中,我们使用了一个候选众数和一个计数器来找出数组中出现次数超过数组长度一半的数字。
1、我们首先假设第一个元素为候选众数,并将计数器初始化为1。然后,遍历数组,如果当前元素与候选众数一样,则计数器加1;如果不同,则计数器减1。当计数器变为0时,我们更新候选众数为当前元素,并将计数器重新设置为1。(因为最坏情况我们最多消去一个众数和一个非众数,遍历到最后剩下计数多的数一定是众数,因为它被消去了仍然是出现最多的数)
2、我们验证候选众数是否为真正的众数,即计算该候选众数在数组中的出现次数,如果大于数组长度的一半,则返回该候选众数。如果不存在超过数组长度一半的数字,则返回-1。
这段代码的时间复杂度为 O(n),其中 n 是数组的长度。由于只使用了常数级别的额外空间,因此空间复杂度是 O(1)。
三、数组中只出现一次的两个数字(含位运算)
题目:
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
数据范围:数组长度2 ≤ n ≤ 1000,数组中每个数的大小 0 < val ≤ 1000000
要求:空间复杂度O(1),时间复杂度O(n)
提示:输出时按非降序排列。
1.哈希法
代码如下:
#include <stdio.h>
#include <stdlib.h>
int* FindNumsAppearOnce(int* nums, int numsLen, int* returnSize)
{
int count[1000000] = { 0 };
*returnSize = 0;
int* result = (int*)malloc(sizeof(int) * 2);
int i = 0;
for (i = 0; i < numsLen; i++)
{
count[nums[i]]++;
}
for (i = 0; i < numsLen; i++)
{
if (count[nums[i]] == 1)
{
result[(*returnSize)++] = nums[i];
}
}
if (result[0] > result[1])
{
int tmp = result[0];
result[0] = result[1];
result[1] = tmp;
}
return result;
}
int main()
{
int arr[] = { 1,4,1,6 };
int len = sizeof(arr) / sizeof(arr[0]);
int size = 0;
int* result = FindNumsAppearOnce(arr, len, &size);
for (int i = 0; i < size; i++)
{
printf("%d ", result[i]);
}
printf("\n");
free(result);
return 0;
}
还是和前面的思路很像,和前面两题有所区别的是这里返回的是一个含两个元素的数组,这就用到了之前学到的动态内存分配的知识。
思路:还是在元素值的对应数组下标上计数,通过访问计数数组找到只出现一次的两个数字,最后根据两个元素的大小调整顺序。
重点还是说一下下面的位运算法。
2.位运算
代码如下(主函数同上):
int* FindNumsAppearOnce(int* nums, int numsLen, int* returnSize)
{
int* result = (int*)malloc(sizeof(int) * 2);
*returnSize = 2;
int tmp = 0;
result[0] = 0;
result[1] = 0;
for (int i = 0; i < numsLen; i++)
{
tmp ^= nums[i]; //tmp是两个只出现一次的数异或的结果
}
int k = 1;
while ((k & tmp) == 0) //tmp按位与1如果为0 说明tmp该位不是1
{
k <<= 1; //直到找到tmp最低位的1(最低位1,表示只出现的两个数在该位是不同的)
}
for (int i = 0; i < numsLen; i++)
{
if ((k & nums[i]) == 0) //如果这一位不为1,分为一组
{
result[0] ^= nums[i];
}
else
{
result[1] ^= nums[i];
}
}
if (result[0] > result[1]) //调整顺序
{
int tmp = result[0];
result[0] = result[1];
result[1] = tmp;
}
return result;
}
思路:
1、由于异或运算可以将相同的数字抵消,而0异或上任何数都是任何数,这时我们用异或运算遍历数组,最后到的值就是两个单独数异或后的结果。即如下代码。
for (int i = 0; i < numsLen; i++)
{
tmp ^= nums[i]; //tmp是两个只出现一次的数异或的结果
}
2、但是我们要得到的是分开后的结果,就可以考虑将数组分成两部分。一个数按位与1如果为0,说明该数的最低位不是1而是0,因为1&1==0,我们先找到这个数最低位的1。即如下代码。
int k = 1;
while ((k & tmp) == 0) //tmp按位与1如果为0 说明tmp该位不是1
{
k <<= 1; //直到找到tmp最低位的1(最低位1,表示只出现的两个数在该位是不同的)
}
3、例如:对于{1,4,1,6}这个数组,异或数组后得到的数就是4异或6的值,即0100异或0110=0010,此时得到tmp最低位的1就是0010。
对于数组的元素,我们就将它们和tmp最低位的1进行比较和划分,如果这一位都为1,分为一组,都为0分为一组。因为相同的数字对应位的数都相等,此时就会把相同的数分在同一组。
这样,{1,4,1,6}这个数组就会分成{6}和{1,4,1},分别在里面异或,得到的就是6和4。即如下代码。
for (int i = 0; i < numsLen; i++)
{
if ((k & nums[i]) == 0) //如果这一位不为1,分为一组
{
result[0] ^= nums[i];
}
else
{
result[1] ^= nums[i];
}
}
4、最后还是对两个元素比较,调整好顺序返回数组。
总结
做了这三题哈希表法的练习后会感觉自己了解并初步学会运用一个非常实用且高效的算法思想,第二题的候选法和第三题的位运算法也非常有趣且实用,让我开拓了思维,了解到更多奇妙的算法思想,收获非常大。