散列表(哈希表)
一、实现一个基于链表法解决冲突问题的散列表
哈希思想就是通过哈希函数f(key)找到在表中的位置。aKey和bKey不相等时,f(aKey) 可能等于 f(bKey)。这种现象称为冲突。 通常有两类方法处理冲突:开放定址法和拉链法。前者是将所有结点均存放在散列表T[0…m-1]中;后者通常是将互为同义词的结点链成一个单链表,而将此链表的头指针放在散列表T[0…m-1]中。
1、开放定址法
开放地址法解决冲突的方法
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
开放寻址法 Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列
从而有三种取法:
di=1,2,3,…,m-1,称线性探测再散列
di=12,-12,22,-22,⑶2,…,±(k)2,(k<=m/2)称二次探测再散列
di=伪随机数序列,称伪随机探测再散列
2、拉链法
拉链法解决冲突的方法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
如图,取表长为8,故散列函数为h(key)=key%8,散列表为T[0…7]。
该方法将所有哈希地址相同的结点构成一个单链表,单链表的头结点存在哈希数组里,链地址法常出现在经常插入和删除的情况下,此时,哈希表的插入/删除/查找都是O(1)的时间复杂度。
#include "stdafx.h"
#include "cstdlib"
#define NULLKEY -1 //空关键字值
#define DELKEY -2 //被删关键字值
typedef int KeyType;
typedef struct
{
KeyType key; //关键字域
int count; //探测次数域
}HashTable;
//插入和建表
//把关键字K插入到哈希表中
void InsertHT(HashTable ha[], int &n, int m, int p, KeyType k)
{
//n为哈希表总元素个数
//p为取余的模,m也就是哈希表创建时总存储空间个数
int i,adr;
adr = k % p; //计算哈希函数值
if (ha[adr].key == NULLKEY || ha[adr].key == DELKEY) //k可以直接放在哈希表中
{
ha[adr].key = k;
ha[adr].count = 1;
}
else
{
i = 1;//i记录k发生冲突的次数
while (ha[adr].key != NULLKEY || ha[adr].key != DELKEY)
{
adr = (adr + 1) % m;
i++;
}
ha[adr].key = k;
ha[adr].count = i;//记录发生冲突次数
}
n++; //哈希表总元素个数
}
void createHT(HashTable ha[], int &n, int m, int p, KeyType keys[], int n1)
{
//n1为keys长度
for (int i = 0; i < m; i++)
{
ha[i].key = NULLKEY;
ha[i].count = 0;
}
n = 0;
for (int i = 0; i < n1; i++)
{
InsertHT(ha, n, m, p, keys[i]);
}
}
//只能在被删元素上做被删标记,不能把被删元素删除
bool deleteHT(HashTable ha[], int &n, int m, int p, KeyType key)
{
int adr;
adr = key % p;
while (ha[adr].key != NULLKEY || ha[adr].key != key)
{
adr = (adr + 1) % m; //线性探测法
}
if (ha[adr].key == key)
{
ha[adr].key = DELKEY;
return true;
}
else
return false;
}
void searchHT(HashTable ha[], int m, int p, KeyType key)
{
int adr;
int i = 1;//查找次数
adr = adr % p;
while (ha[adr].key != NULLKEY && ha[adr].key != key)
{
i++; //累计关键字的比较次数
adr = (adr + 1) % m;//线性探测
}
if (ha[adr].key == key)
printf("Search Successfully! 关键字%d,比较%d次\n", key, i);
else
printf("Search failed! 关键字%d,比较%d次\n", key, i);
}
void ASL(HashTable ha[], int n, int m, int p)
{
int i, s,j;
int success,unsuccess;
for (i = 0; i < m; i++)
{
if (ha[i].key != NULLKEY)
success += ha[i].count; //count为查询该关键字的时候比较次数
}
printf("成功情况下ASL(%d)=%g\n", n, success*1.0 / n);
for (i = 0; i < p; i++)
{
//s记录该关键字的探测次数
//j为移动的指针
s = 1; j = i;
while (ha[j].key != NULLKEY)
{
s++;
j = (j + 1) % m;
}
unsuccess += s;
}
printf("不成功情况下ASL(%d)=%g\n", n, unsuccess*1.0 / p);
}
typedef int KeyType;
typedef struct node
{
struct node *next;
KeyType key;
}NodeType;
typedef struct
{
NodeType *firstp;
}HashTable2;
void insertHT2(HashTable2 ha[], int &n, int p, KeyType k)
{
int adr;
adr = k % p;
NodeType *q;
q = (NodeType*)malloc(sizeof(NodeType));
q->next = NULL;
q->key = k;
if (ha[adr].firstp == NULL)//若单链表adr为空
ha[adr].firstp = q;
else //头插法
{
q->next = ha[adr].firstp;
ha[adr].firstp = q;
}
n++; //哈希表中元素个数
}
void createHS(HashTable2 ha[], int &n, int m, int p, KeyType keys[], int n1) //n1为keys列表长度
{
for (int i = 0; i < m; i++)
ha[i].firstp = NULL;
n = 0;
for (int i = 0; i < n1; i++)
insertHT2(ha, n, p, keys[i]);
}
//删除算法,要考虑首结点是否为所要删除的结点,若不是,另外开辟前驱结点prep来删除
bool deleteHS2(HashTable2 ha[], int &n, int m, int p, KeyType k)
{
int adr;
adr = k % p;
NodeType *q, *prep;
q = ha[adr].firstp;
if (q == NULL)
return false;
if (q->key == k)
{
ha[adr].firstp = q->next;
free(q);
n--;
return true;
}
//走到了这一步,说明首结点不为k
prep = q; q = q->next;
while (q != NULL)
{
if (q->key == k)
{
break;
}
q = q->next;
}
if (q != NULL)
{
prep->next = q->next;
free(q);
n--;
return true;
}
else return false;
}
void searchHS2(HashTable2 ha[], int p, KeyType k)
{
NodeType *q;
int i=0,adr;
adr = k % p;
q = ha[adr].firstp;
while (q != NULL)
{
i++;
if (q->key == k)
break;
q = q->next;
}
if (q != NULL)
printf("Search successfully %d 比较%d次\n", k, i);
else
printf("failed %d比较%d次\n", k, i);
}
void ASL2(HashTable2 ha[], int n, int m)
{
int success, unsuccess;
NodeType *q;
for (int i = 0; i < m; i++)
{
q = ha[i].firstp;
int s = 0;
while (q != NULL)
{
s++;
q = q->next;
success += s;
}
unsuccess += s;//走到这一步,说明找不到
}
}
#define MAXSIZE 100
typedef int KeyType;
typedef struct
{
KeyType key;
int link;
}IdxType;
int idxSearch(IdxType I[], int b, int R[], int n, KeyType k)
{
int s = (n + b - 1) / b; //s为每块的元素个数,应为[n/b] 例如25个元素,分为5块,那么每块五个元素
int low, high = b- 1,i; //先在索引表中进行折半查找
while (low <= high)
{
int mid = (low + high) / 2;
if (I[mid].key >=k)
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
//应该先在索引表的high+1块中寻找,再在主数据表中进行顺序查找
i = I[high + 1].link;
while (i<=I[high + 1].link + s - 1 && R[i] != k)
{
i++;
}
if (i <= I[high + 1].link + s - 1)
return i + 1;
else return 0;
}
二、实现一个 LRU 缓存淘汰算法
缓存是一种提高数据读取性能的技术,比如常见的cpu缓存以及浏览器缓存!但是缓存的大小有限,当缓存用满的时候,哪些数据应该被清理出去,哪些数据应该被保留?
解决方案:FIFO—>先进先出 LFU—> 最少使用 LRU–>最近最少使用
如何使用链表实现LRU缓存淘汰策率呢?
LRU的设计原理就是,当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,我们需要然其能够快速命中,而不常访问的数据,我们在容量超出限制内,要将其淘汰。
思路:
每次访问的数据都会放在栈顶,当访问的数据不在内存中,且栈内数据存储满了,我们就要选择移除栈底的元素,因为在栈底部的数据访问的频率是比较低的。所以要将其淘汰。
public class LRUCache {
//“潜水”链表节点,抽象
static class Node{
//键值对
private int key;
private int value;
//维护“潜水”键值对,双向链表
private Node pre;
private Node next;
//构造器
Node(){}
Node(int key,int value){
this.key = key;
this.value = value;
}
}
//指定的容量
private int cap;
//保留“潜水”双向链表的头尾指针
private Node head;
private Node tail;
//保存键值对的map
private HashMap<Integer,Node> map;
//构造器参数是:指定的容量
public LRUCache(int capacity) {
this.cap = capacity;
//初始化头尾节点,这里的头结点是辅助节点
//head节点不存储任何有效元素
head = new Node();
tail = head;
//构造器初试容量这样设置可以保证map
//不会发生扩容,详见之前的HashMap
//讲解文章
map = new HashMap<>((int)(cap/0.75)+1);
}
//将指定节点从链表中删除
private void removeNode(Node cur){
if(cur==tail){
tail = tail.pre;
tail.next = null;
cur.pre = null;
}else{
cur.pre.next = cur.next;
cur.next.pre = cur.pre;
cur.pre = null;
cur.next = null;
}
}
//将指定节点追加到链表末尾
private void add(Node cur){
tail.next = cur;
cur.pre = tail;
tail = cur;
}
//访问一个键值对
public int get(int key) {
Node cur = map.get(key);
//不存在这个key
if(cur==null){
return -1;
}else{//存在
//含义是当前潜水节点已经被访问了
//将这个节点添加到链表末尾
removeNode(cur);
add(cur);
return cur.value;
}
}
//存储一个键值对
public void put(int key, int value) {
Node cur = map.get(key);
if(cur==null){
//put前不存在这个key
cur = new Node(key,value);
//将该键值对移动到链表末尾
map.put(key,cur);
add(cur);
//超出了容量,移除链表头结点
//后面那个元素(头结点是辅助节点)
if(map.size()>cap && head!=tail){
Node outDate = head.next;
removeNode(outDate);
//不能忘记这里
map.remove(outDate.key);
}
}else{
//put之前已经存在
//将这个键值对移到链表末尾即可
removeNode(cur);
add(cur);
//更新这个key的值
cur.value = value;
}
}
}