拉链法(open hashing)和开地址法(closed hashing或者opened addressing)

拉链法,我们可以理解为 “链表的数组”(转自Java 中的 ==, equals 与 hashCode 的区别与联系

如图:

 这里写图片描述

 左边很明显是个数组,数组的每个成员是一个链表。该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。先调用这个元素的 hashCode 方法,然后根据所得到的值计算出元素应该在数组的位置。如果这个位置上没有元素,那么直接将它存储在这个位置上;如果这个位置上已经有元素了,那么调用它的equals方法与新元素进行比较:相同的话就不存了,否则,将其存在这个位置对应的链表中(Java 中 HashSet, HashMap 和 Hashtable的实现总将元素放到链表的表头)。

拉链法的适用范围 : 快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。


开放定址法(线性探测)

即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。比较常用的探测方法有线性探测法,比如有一组关键字{12,13,25,23,38,34,6,84,91},Hash表长为14,Hash函数为address(key)=key%11,当插入12,13,25时可以直接插入,而当插入23时,地址1被占用了,因此沿着地址1依次往下探测(探测步长可以根据情况而定),直到探测到地址4,发现为空,则将23插入其中。(转自哈希表详解(知识点拾遗,Top K算法详解))

下面的例子有助于理解。(转自哈希表详解(附实现代码))

将关键字序列{7, 8, 30, 11, 18, 9, 14}散列存储到散列表中。散列表的存储空间是一个下标从0开始的一维数组,长度为10,即{0, 1,2, 3, 4, 5, 6, 7, 8, 9}。散列函数为: H(key) = (key * 3) % 7,处理冲突采用线性探测再散列法。

求等概率情况下查找成功和查找不成功的平均查找长度。

解:

1 求散列表

H(7) = (7 * 3) % 7 = 0

H(8) = (8 * 3) % 7 = 3

H(30) = 6

H(11) = 5

H(18) = 5

H(9) = 6

H(14) = 0

按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。

address0123456789
key714 8 1130189 

插入key = 18时,根据H(18) = 5应插在addresss=5的位置,但是address=5已经被key=11占据了,所以往后挪一位到address=6的位置,但是address=6被key=30占据了,再往后挪一位到address=7的位置,这个位置是空的,所以key=18就插到这个位置。

插入key = 9时,根据H(9) = 6应插在address=6的位置,但address = 6已经被key = 30占据,所以需要往后挪一位到address = 7的位置,但是address = 7已经被key = 18占据,所以再往后挪移到address = 8的位置,这个位置是空的,所以key = 9就插到这个位置。

插入key=14时,根据H(14) = 0应插在address=0的位置,但address=0被key=7占据,所以往后挪移一位到address=1的位置,这个位置是空的,所以key=14就插到这个位置。

2 求查找成功的平均查找长度

查找7,H(7) = 0,在0的位置,一下子就找到了7,查找长度为1。

查找8,H(8) = 3,在3的位置,一下子就找到了8,查找长度为1。

查找30,H(30) = 6,在6的位置,一下子就找到了30,查找长度为1。

查找11,H(11) = 5,在5的位置,一下子就找到了11,查找长度为1。

查找18,H(18) = 5,第一次在5的位置没有找到18,第二次往后挪移一位到6的位置,仍没有找到,第三次再往后挪移一位到7的位置,找到了,查找长度为3。

查找9,H(9) = 6,第一次在6的位置没找到9,第二次往后挪移一位到7的位置,仍没有找到,第三次再往后挪移一位到8的位置,找到了,查找长度为3.

查找14,H(14) = 0,第一次在0的位置没找到14,第二次往后挪移一位到1的位置,找到了,查找长度为2。

address0123456789
key714 8 1130189 
length12 1 1133 

所以,查找成功的平均查找长度为(1 + 1 + 1 + 1 + 3 + 3 + 2) / 7 = 12 / 7。

3 求查找不成功的平均查找长度

address0123456789
key714 8 1130189 

查找不成功,说明要查找的数字肯定不在上述的散列表中。

因为这里哈希函数的模为7,所以要查找的数只可能位于0~6的位置上。

(1)若要查找的数key对应的地址为0,有(key * 3) % 7 = 0。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 28。
第一次查找,address = 0时key = 7,不是要找的28,
第二次查找,往后挪移一位,address = 1时key = 14,不是要找的28;
第三次查找,往后再挪移一位,address = 2时key为空。可知查找不成功,否则28应该放在adress = 2的位置上。
结论:查找3次可知查找不成功。
(2)若要查找的数key 对应的地址为1,有(key * 3) % 7 = 1。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 5。
第一次address = 1时key = 14,不是要找的5
第二次adress = 2时key为空。可知查找不成功,否则key = 5应该放在adress=1的位置上。
结论:查找2次可知查找不成功。
(3)若要查找的数key对应的地址为2,有(key * 3) % 7 = 2。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 3。
第一次查找,address = 2时key为空。可知查找不成功,否则key = 3应该放在address = 2的位置。
结论:查找1次可知查找不成功。
(4)若要查找的数key对应的地址为3,有(key * 3) % 7 = 3。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 15。
第一次查找,address = 3时key = 8,不是要找的15.
第二次查找,往后挪移一位,address = 4时key为空。可知查找不成功,否则key = 15会放在address = 4的位置上。
结论:查找2次可知查找不成功。
(5)若要查找的数key对应的地址为4,有(key * 3) % 7 = 4。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 6。
第一次查找,address = 4时key为空。可知查找不成功,否则key = 6会放在address = 4的位置上。
结论:查找1次可知查找不成功。
(6)若要查找的数key对应的地址为5,有(key * 3) % 7 = 5。
因为key不属于{7, 8, 30, 11, 18, 9, 14},可设key = 4。
第一次查找,address = 5时key = 11,不是要找的4.
第二次查找,往后挪移一位,address = 6时key=30,不是要找的4。
第三次查找,往后再挪移一位,注意此时address = 0而非address = 7,因为模为7,决定了要查找的数只可能位于0~6的位置上。address = 0时key = 7,不是要找的4。
第四次查找,往后再挪移一位,address = 1时key = 14,不是要找的4。
第五次查找,往后再挪移一位,address = 2时key为空。可知查找不成功,否则key = 4会放在address = 2的位置上。
结论:查找5次可知查找不成功。
(7)若要查找的数key对应的地址为5,同理可得出结论:查找4次可知查找不成功。

综上,查找不成功的次数表如下所示

address0123456
count3212154

所以,查找不成功的平均查找长度为(3 + 2 + 1 + 2 + 1 + 5 + 4)/ 7 = 18 / 7

优点:

不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。

哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。

如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。

缺点:

它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据,或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程。


内容来源:

    Java 中的 ==, equals 与 hashCode 的区别与联系

    哈希表详解(知识点拾遗,Top K算法详解)

    哈希表详解(附实现代码)

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
实现围棋人机对弈可以分为两个部分:棋盘表示和博弈树搜索算。 棋盘表示可以使用二维数组来表示,其中0表示空位,1表示黑子,2表示白子。为了在搜索中加速,可以使用Zobrist Hashing技术对棋盘状态进行哈希。 Zobrist Hashing是一种快速计算哈希值的方,它使用随机数对棋盘上的每个点进行哈希,然后通过异或运算将它们组合起来得到最终的哈希值。在每次搜索时,我们可以记录当前的哈希值,以便快速判断重复状态。 博弈树搜索算可以使用极大极小算和Alpha-Beta剪枝来实现。具体的实现步骤如下: 1. 构建博弈树:从当前的棋盘状态出发,生成所有可能的下一步棋盘状态,然后将它们作为子节点添加到当前节点。这样就构建了一棵博弈树。 2. 评估函数:对于每个叶子节点,我们需要使用评估函数来评估当前棋盘状态的好坏程度。评估函数可以考虑棋子的数量、位置、形状等因素。 3. 极大极小算:从根节点始,按照极大极小原则逐层计算每个节点的值。对于极大节点,选择子节点中最大的值作为自己的值;对于极小节点,选择子节点中最小的值作为自己的值。 4. Alpha-Beta剪枝:为了加速搜索,我们可以使用Alpha-Beta剪枝来减少搜索的分支。具体来说,如果当前节点的值已经超出了父节点的期望值,那么我们就可以停止搜索。 5. 最佳着:最后,从根节点出发,选择值最大的子节点作为最佳着。 代码实现可以参考以下伪代码: ``` python # 定义棋盘大小和随机数表 BOARD_SIZE = 15 ZOBRIST_SIZE = BOARD_SIZE * BOARD_SIZE * 2 zobrist = [[0] * ZOBRIST_SIZE for _ in range(3)] # 初始化随机数表 def init_zobrist(): for i in range(BOARD_SIZE): for j in range(BOARD_SIZE): for k in range(2): zobrist[k+1][i*BOARD_SIZE+j] = random.randint(0, 2**64-1) # 计算棋盘状态的哈希值 def get_hash(board): hash_val = 0 for i in range(BOARD_SIZE): for j in range(BOARD_SIZE): if board[i][j] == 1: hash_val ^= zobrist[1][i*BOARD_SIZE+j] elif board[i][j] == 2: hash_val ^= zobrist[2][i*BOARD_SIZE+j] return hash_val # 极大极小算 def min_max(board, depth, alpha, beta, is_max): # 到达搜索深度或者游戏结束 if depth == 0 or is_game_over(board): return evaluate(board) # 获取所有可行的着 moves = get_legal_moves(board) if len(moves) == 0: return evaluate(board) # 对于极大节点,选择子节点中最大的值 if is_max: max_val = -float('inf') for move in moves: # 创建子节点 child_board = make_move(board, move) # 计算子节点的值 val = min_max(child_board, depth-1, alpha, beta, False) # 更新最大值 if val > max_val: max_val = val # Alpha-Beta剪枝 alpha = max(alpha, max_val) if alpha >= beta: break return max_val # 对于极小节点,选择子节点中最小的值 else: min_val = float('inf') for move in moves: # 创建子节点 child_board = make_move(board, move) # 计算子节点的值 val = min_max(child_board, depth-1, alpha, beta, True) # 更新最小值 if val < min_val: min_val = val # Alpha-Beta剪枝 beta = min(beta, min_val) if beta <= alpha: break return min_val # 获取最佳着 def get_best_move(board, depth): moves = get_legal_moves(board) if len(moves) == 0: return None best_move = moves[0] max_val = -float('inf') for move in moves: # 创建子节点 child_board = make_move(board, move) # 计算子节点的值 val = min_max(child_board, depth-1, -float('inf'), float('inf'), False) # 更新最佳着 if val > max_val: best_move = move max_val = val return best_move ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值