从两个数组中查找相同的数字谈Hashtable

本文探讨了在数组查找中的效率优化,通过引入哈希技术,将数组元素的查找时间复杂度从O(n)降低至O(1),并详细解释了哈希表、一致性哈希算法以及哈希碰撞解决方法。同时,文章还讨论了哈希技术在数据库、前端开发、后端开发等领域的广泛应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题的起因

假设数组A有n个元素,数组B有n个元素。

看到这种题的时候,我们最直观的就是通过两层for循环来对比每个数组中的数字。因此A数组中的每个元素都会和B数组中的每个元素对比过一次,所以总共要对比的次数是n个n相加(或者是n个m相加),也就是n2(或者为n x m).

因此我们想能不能有更快的方法呢?让其中一个数组的查找的时间复杂度不再是O(n)就可以了。也就是我们在这个数组中查找一个数,不是通过遍历的方式。

但是不是通过遍历的方式能在一个数组中找到一个自己想要的数吗?看起来必须有什么特殊的方法才行。

我们再回过头来看数组是什么组成的:

1.下标

2.下标所代表的元素

 

我们按位置查找时,数组的速度是O(1)的,因为我们只需要通过下标就可以定位那个元素。否则需要采用遍历的O(n)的方式。

O(n)看起来好像速度还可以,其实想想看还真没有比它更慢的了,你都把遍历完一遍了当然知道有没有所需要的元素了。

这时,我们就想到,如果用下标位置来标示所需的数字呢,而元素用来标记是否出现,出现1次就加1。比如说数组A中存在元素12,就在由数组B转换后的数组C中检查C[12]是否等于1.

我们把数组B转换成数组C,所需要的时间也为O(n)

那就使得对A数组的遍历仍为O(n),但检查数组A中的元素是否存在于数组C中则由原来的O(n)转换为O(1).

所以总的时间复杂度为O(n+n)。

 

看起来不错,但是问题来了,如果A中的数字很大并且很分散时,会造成数组C中大量的空间被浪费。比如说数组A是由{1,333,666}这三个元素,那么辅助数组C就需要667的大小,但是只用到了其中的3个,也就是664个空间是白浪费的。

这种直接通过数组下标寻址的方法,在数组元素非连续的情况下,会造成大量空间浪费。

 

 

那什么情况下数组中的数字会比较连续呢?还记得这张图吧,不记得的点这里

 

也就是ASCII码。

所以这种方式除了用来查找数字,也可以用来查找字母

代码如下:(赶时髦用C来写,写的过程中感觉各种不适应啊。注:"abc"是作为指针传递,'abc'是作为整数传递,数组在作为参数时会退化为指针。)

 

#include <stdio.h>
#include <stdlib.h>


char * FindSameChar(char *stringA, char *stringB){
if(stringA == NULL || stringB == NULL ){
return '\0';
}

const int containerSize = 128;
unsigned int container[containerSize];
for (unsigned int i=0; i<containerSize; i++)
{
container[i] = 0;
}

char *result = (char *)malloc(sizeof(stringA));
char *key = stringA;
while(*key != '\0')
container[*(key++)]++;

key = stringB;
char *findResult = result;
while(*key != '\0')
{

if(container[*key] >= 1){
*findResult = *key;
findResult++;
}

key++;
}

if (result != NULL)
return result;
else
return '\0';
}

 空间复杂度为O(1),因为这里我开的辅助内存大小为128是个常数。

 

这种下标和数字紧耦合的方式使得我们这种方法受到了极大的限制,那么我们有没有办法“解耦”呢?

也就是说,让1,和666不要离开这么远,中间空了665个空位。让他们尽可能的靠近些。这时候就需要通过一种方法来实现这种解耦,也就是说通过某个函数算出相应的位置,使得1和666之间的空格数尽量更小。

这就是hash函数。

 

取模算法

 hash技术很好的解决了直接寻址遇到的问题,但是这么做同时也引入了新的问题,通过hash函数算出来的位置可能会相同,一般就称为发生了“碰撞"。也就是虽然我们希望通过hash函数使得1和666的位置尽量能靠近些,但是可能某些hash函数算出来的结果就是1和666位置相同。一般发生这种情况有几种方式解决,二次hash和链接法。链接法用的是最多的,就是在同一个位置上创建一个链表。

当然如果碰撞多了,hash表也变成了链结构,会极大的影响效率。当初某个BUG就是因为采用了某种hash算法导致产生大量的碰撞,使得服务器性能下降。

 

hash表用的地方非常多,比如数据库中就是用了哈希表,通常采用的是除法hash方式。

在用来设计哈希函数的除法散列法中,通过取k除以m的余数,来将关键字K映射到m个位置中的某个位置,也就是:

  hash(k) = k mod m;

顺便值得一提的是,获取余数是个经常用到的方法,比如说两人跑步套圈,迷宫中的右手扶墙算法等。也就是说能让一个数不停的在一个指定的范围内打转。

.NET中相应的实现是HashSet<T>,Dictionary<TKey,TValue>,Hashtable。

当然这几种结构肯定有区别,下篇还要谈谈Equals和GetHashcode再说吧。

 

一致性Hash算法

如上面所说,最常规的方式莫过于hash取模的方式。比如集群中可用机器适量为N,那么key值为K的的数据请求很简单的应该路由到hash(K) mod N对应的机器。的确,这种结构是简单的,也是实用的。但是在一些高速发展的web系统中,这样的解决方案仍有些缺陷。随着系统访问压力的增长,缓存系统不 得不通过增加机器节点的方式提高集群的相应速度和数据承载量。增加机器意味着按照hash取模的方式,在增加机器节点的这一时刻,大量的缓存命不中,缓存 数据需要重新建立,甚至是进行整体的缓存数据迁移,瞬间会给DB带来极高的系统负载,设置导致DB服务器宕机。
 

分布式缓存设计核心点:在设计分布式cache系统的时候,我们需要让key的分布均衡,并且在增加cache server后,cache的迁移做到最少。这里提到的一致性hash算法ketama的做法是:选择具体的机器节点不在只依赖需要缓存数据的key的hash本身了,而是机器节点本身也进行了hash运算。

转载于:https://www.cnblogs.com/lwzz/archive/2012/02/15/2350903.html

### C语言数组查找重复元素的方法代码示例 在C语言中,查找数组中的重复元素可以通过多种方法实现。以下是几种常见的方法及其实现代码。 #### 方法一:使用双重循环 通过两层嵌套循环来比较数组中的每个元素是否其他元素相同。如果找到相同的元素,则认为它是重复的。 ```c #include <stdio.h> int main() { int arr[] = {1, 2, 3, 4, 5, 2, 6, 7, 8, 9}; int n = sizeof(arr) / sizeof(arr[0]); int isDuplicate; printf("重复的元素有: "); for (int i = 0; i < n - 1; i++) { isDuplicate = 0; for (int j = i + 1; j < n; j++) { if (arr[i] == arr[j]) { printf("%d ", arr[i]); isDuplicate = 1; break; } } if (isDuplicate) { // 避免重复输出相同的重复元素 for (int k = i + 1; k < n; k++) { if (arr[k] == arr[i]) { arr[k] = -1; // 标记为已处理 } } } } return 0; } ``` 这种方法的时间复杂度为 O(n²),适用于小规模据[^1]。 --- #### 方法二:使用排序后查找 先对数组进行排序,然后遍历排序后的数组,检查相邻的两个元素是否相等。如果相等,则说明存在重复元素。 ```c #include <stdio.h> // 冒泡排序函 void bubbleSort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int arr[] = {1, 2, 3, 4, 5, 2, 6, 7, 8, 9}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); printf("重复的元素有: "); for (int i = 0; i < n - 1; i++) { if (arr[i] == arr[i + 1]) { printf("%d ", arr[i]); i++; // 跳过下一个相同的元素 } } return 0; } ``` 此方法的时间复杂度主要由排序决定,冒泡排序的时间复杂度为 O(n²),但可以使用更高效的排序算法(如快速排序)降低时间复杂度[^5]。 --- #### 方法三:使用哈希表(数组模拟) 通过创建一个辅助数组作为哈希表,记录每个元素出现的次。对于数组中的每个元素,将其值作为索引,在哈希表中对应位置加 1。最后检查哈希表中值大于 1 的位置,即为重复元素。 ```c #include <stdio.h> #define MAX_VALUE 100 // 假设数组元素范围为 0 到 MAX_VALUE-1 int main() { int arr[] = {1, 2, 3, 4, 5, 2, 6, 7, 8, 9}; int n = sizeof(arr) / sizeof(arr[0]); int hashTable[MAX_VALUE] = {0}; for (int i = 0; i < n; i++) { hashTable[arr[i]]++; } printf("重复的元素有: "); for (int i = 0; i < MAX_VALUE; i++) { if (hashTable[i] > 1) { printf("%d ", i); } } return 0; } ``` 此方法的时间复杂度为 O(n),但需要额外的空间来存储哈希表。适合于元素值范围较小的情况[^4]。 --- ### 注意事项 - 如果数组中的元素值范围较大,方法三可能不适用,因为会消耗大量内存。 - 方法二虽然需要排序,但如果使用高效的排序算法(如快速排序),其性能通常优于双重循环。 - 方法一简单直观,但在大规模据下效率较低。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值