对于散列表的定义,实现,以及冲突的处理,学过数据结构的肯定非常熟悉。下面借助leetcode上一道简单题目来浅谈如何散列进行查找。这是典型的利用空间来换取时间的例子。
问题描述
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
example
Given nums = [2, 7, 11, 15], target = 9
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
暴力实现
这个题目可以暴力求解,遍历所有的元素对,如果其和符合要求,输出索引,然后退出程序。实现程序如下:
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* twoSum(int* nums, int numsSize, int target) {
int i,j;
int *result = (int *)malloc(sizeof (int) * 2);
for(i = 0; i < numsSize; ++i)
for(j = i + 1; j < numsSize; ++j)
{
if(nums[i] + nums[j] == target)
{
result[0] = i;
result[1] = j;
}
}
return result;
}
暴力法虽然实现简单,但是效率不高,leetcode给出的结果是:runtime beats 28.73% of c submissions。既然没有动脑也不能奢求很高的运行效率。最坏情况下需要比较所有的元素对,比较次数是n * (n - 1) / 2, 时间复杂度为O(n^2)。平均情况下时间复杂度为哦O(n^2)。
为了提高查找效率,下面对暴力法进行改进。
散列查找
1. 散列的定义:
散列,就是Hash,把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
从以上的定义中,可以提取出2条重要信息:一是散列就是数学中的映射,二是散列发结果可能产生冲突,不能通过散列结果反过来查找输入值。
2. 常见的散列函数:
设计散列函数主要考虑的问题就是怎么减少冲突。对于不同的应用,有不同的合适的散列函数,这里只介绍一种常见的散列函数—–取余法。
取关键字被数p除后所得余数为哈希地址:H(key)=key MOD p (p≤m)
这是一种最简单,也最常用的构造哈希函数的方法。它不仅可以对关键字直接取模(MOD),也可在折迭、平方取中等运算之后取模。值得注意的是,在使用除留余数法时,对p的选择很重要。一般情况下可以选p为质数或不包含小于20的质因素的合数。
3. 冲突处理
散列的过程中不可避免的会产生冲突,下面介绍2种处理冲突的方法:
1)开放定值法
基本思想:将所有结点均存放在散列表[0..m-1]中。当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址 (即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的地址则表明表中无待查的关键字,即查找失败。
2) 链地址法
基本思想:将互为同义词的结点链成一个单链表,而将此链表的头指针放在散列表[0..m-1]中。
例:已知一组关键字为(19,14,23,01,68,20,84,27,55,11,10,79),则按哈希函数H(key)=key MOD13和链地址法处理冲突构造所得的哈希表为:
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
// 利用散列表来实现
#include <stdlib.h>
typedef struct HashNode
{
int key;
int index;
}HashNode;
typedef struct HashMap
{
int size;
HashNode **storage;
}HashMap;
HashMap *create_hash(int size)
{
HashMap *hashmap;
hashmap = (HashMap *)malloc(sizeof(HashMap));
assert(hashmap != NULL);
hashmap -> size = size;
hashmap -> storage = (HashNode **) malloc(sizeof(HashNode *) * size);
assert(hashmap -> storage != NULL);
/*for(int i = 0; i < size; ++i)
{
hashmap -> storage[i] = NULL;
}*/
memset(hashmap -> storage, 0, sizeof(HashNode *) * size);
return hashmap;
}
void destory_hash(HashMap * hashmap)
{
int i;
for(i = 0; i < hashmap -> size; ++i)
{
if (hashmap -> storage[i] != NULL)
free(hashmap -> storage[i]);
}
free(hashmap -> storage);
free(hashmap);
}
void set_hash(HashMap *hashmap, int key, int index)
{
int hash = abs(key) % hashmap -> size;
HashNode *node;
while((node = hashmap -> storage[hash]) != NULL)
{
if(hash < hashmap -> size - 1)
++hash;
else
hash = 0;
}
node = (HashNode *)malloc(sizeof (HashNode));
assert(node != NULL);
node -> key = key;
node -> index = index;
hashmap -> storage[hash] = node;
}
HashNode *get_hash(HashMap * hashmap, int key)
{
int hash = abs(key) % hashmap -> size;
HashNode *node;
while((node = hashmap -> storage[hash]))
{
if(node -> key == key)
return node;
if(hash < hashmap -> size - 1)
++hash;
else
hash = 0;
}
return NULL;
}
int *twoSum(int* nums, int numsSize, int target)
{
HashMap *hashmap;
HashNode *node;
int rest, i;
int *result = (int *) malloc(sizeof(int )* 2);
hashmap = create_hash(numsSize * 2);
for(i = 0; i < numsSize; ++i)
{
rest = target - nums[i];
node = get_hash(hashmap, rest);
if(node != NULL)
{
result[0] = i;
result[1] = node -> index;
break;
}
else
set_hash(hashmap, nums[i], i);
}
return result;
}
利用散列查找,leetcode给出的结果是:runtime beats 90.05% of c submissions。上面的代码利用了C中的结构类型,需要对指针的链表有一定的了解。对链表的讨论超过了这篇博客的范围,这里就不详细叙述了。
最后讨论一下散列查找的效率:把key进行散列的时间复杂度为:O(n),进行查找的复杂度为O(n)。最后时间复杂度为O(n)。