5.查找
5.1 查找的基本概念
(1)查找表——查找表是由同一类型的数据元素(或记录)构成的集合
(2)关键字——关键字是数据元素(或记录)中某个数据项的值,用它可以标识一个数据元素(或记录)。若此关键字可以唯一地标识一个记录,则称此关键字为主关键字;用以识别若干记录的关键字为次关键字
(3)查找——查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素
(4)动态查找表和静态查找表——若在查找的同时对表做修改操作(如插入或删除),则相应的表称之为动态查找表;否则,称之为静态查找表
(5)平均查找长度——为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度()。对于含有 个记录的表,查找成功时的平均查找长度为
其中, 为查找表中其关键字与给定值相等的第 个记录的概率,且 ; 为找到表中其关键字与给定值相等的第 个记录时,和给定值已进行过比较的关键字个数
5.2 线性表的查找
5.2.1 顺序查找
顺序查找的查找过程为: 从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功;反之,若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
顺序查找方法既适用于线性表的顺序存储结构,又适用于线性表的链式存储结构。
优点: 算法简单,对表结构无任何要求,无论记录是否有序均可应用
缺点: 平均查找长度较大,查找效率较低,所以当 很大时,不宜采用顺序查找
顺序查找算法分析:
设置监视哨免去查找过程中每一步都要检测整个表是否查找完毕。时间复杂度为 。平均查找长度为:
/**
* 顺序查找
*/
int Search_Seq(SSTable ST,int key){
for(int i=ST.length;i>=1;i--){
if(ST.elems[i] == key){
return i;
}
}
return 0;
}
/**
* 设置哨兵的顺序查找
*/
int Search_Seq2(SSTable ST,int key){
ST.elems[0] = key;
int i = ST.length;
for(;ST.elems[i]!=key;i--);
return i;
}
class SSTable{
int[] elems;
int length;
public SSTable(int length) {
this.length = length;
this.elems = new int[length+1];
}
}
5.2.2 折半查找
折半查找也称二分查找。要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
优点: 比较次数少,查找效率高
缺点: 只能用于顺序存储的有序表,查找前要排序;不适用于数据元素经常变动的线性表
算法步骤:
(1)置查找区间初值, 为 , 为表长
(2)当 小于等于 时,循环执行以下操作:
- 取值为 和 的中间值
- 将给定值 与中间位置记录的关键字进行比较,若相等则查找成功,返回中间位置
- 若不相等则利用中间位置记录将表对分成前、后两个子表。如果 比中间位置记录的关键字小,则 取为 ,否则 取为
(3)循环结束,说明查找区间为空,则查找失败,返回 0
算法分析:
把当前查找区间的中间位置作为根,左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的判定树。
借助判定树,可求得ASL。为了讨论方便起见,假定有序表的长度 ,则判定树是深度为 的满二叉树。树中层次为 的结点有 1 个,层次为 的结点有 2 个,...,层次为 的结点有 个。假设表中每个记录的查找概率相等 ,则查找成功时折半查找的平均查找长度为:
推理:错位相消法
当 较大时,可有下列近似结果:
/**
* 二分查找,查找一个数
*/
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
/**
* 寻找左侧边界的二分搜索
*/
int binarySearch_LeftBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
// 收缩右侧边界
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
if (left == nums.length) return -1;
return nums[left]==target ? left:-1;
}
/**
* 寻找右侧边界的二分查找
*/
int binarySearch_RightBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
// 收缩左侧边界
left = mid + 1;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
if (left - 1 < 0) return -1;
return nums[left-1]==target ? left-1:-1;
}
5.2.3 分块查找
分块查找又称索引顺序查找,介于顺序查找和折半查找之间的一种查找方法。在此查找法中,除表本身以外,尚需建立一个“索引表”。对每个子表建立一个索引项,其中包括两项内容:关键字项(其值为该子表内的最大关键字)和指针项(指示该子表的第一个记录在表中位置)。索引表按关键字有序,则表或者有序或者分块有序。
在索引表中可通过折半查找,在块中只能是顺序查找。
一般情况下,为进行分块查找,可以将长度为 的表均匀地分成 块,每块含有 个记录,即 ;又假定表中每个记录的查找概率相等,则每块查找的概率为 ,块中每个记录的查找概率为 。分块查找的平均查找长度为:
优点: 在表中插入和删除数据元素时,只要找到该元素对应的块,就可以在该块内进行插入和删除运算。如果线性表既要快速查找又经常动态变化,则可采用分块查找
缺点: 要增加一个索引表的存储空间并对初始索引表进行排序运算
5.3 树表的查找
5.3.1 二叉排序树
二叉排序树(Binary Sort Tree) 又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。
1.二叉树排序树的定义
(1)若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根节点的值
(3)它的左、右子树也分别为二叉排序树
中序遍历二叉排序时可以得到一个有序序列。
2.二叉排序树的查找
算法步骤:
(1)若二叉排序树为空,则查找失败,返回空指针
(2)若二叉排序树非空,将给定值 与根节点的关键字 进行比较:
- 若 等于 ,则查找成功,返回根节点地址
- 若 小于 ,则递归查找左子树
- 若 大于 ,则递归查找右子树
算法分析:
当先后插入的关键字有序时,构成的BST是单支树。树的深度为 ,其平均查找长度为 (和顺序查找相同)。
最好的情况是,BST的形态和折半查找的判定树相似,其平均查找长度为 。
对于需要经常进行插入、删除和查找运算的表,采用二叉排序树比较好。
/**
* BST 查找
*/
TreeNode Search_BST(TreeNode root,int key){
if(root == null){
return null;
}
if(root.val == key){
return root;
}else if(root.val < key){
return Search_BST(root.right,key);
}else{
return Search_BST(root.left,key);
}
}
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
3.二叉排序树的插入
算法步骤:
(1)若二叉排序树为空,则待插入结点作为根节点插入到空树中
(2)若二叉排序树非空,则将 与根结点的关键字 进行比较:
- 若 小于 ,则将待插入结点插入左子树
- 若 大于 ,则将待插入结点插入右子树
算法分析:
二叉排序树插入的基本过程是查找,所以时间复杂度同查找一样,是 。
/**
* BST 插入
*/
TreeNode InsertBST(TreeNode root,int key){
if(root == null){
return new TreeNode(key);
}
if(root.val < key){
root.right = InsertBST(root.right,key);
}else if(root.val > key){
root.left = InsertBST(root.left,key);
}
return root;
}
4.二叉排序树的创建
算法步骤:
(1)将二叉排序树 初始化为空树
(2)读入一个关键字为 的结点
(3)如果读入的关键字 不是输入结束标志,则循环执行以下操作:
- 将此结点插入二叉排序树 中
- 读入一个关键字为 的结点
算法分析:
假设有 个结点,则需要 次插入操作,而插入一个结点的算法时间复杂度为 ,所以创建二叉排序树算法的时间复杂度为 。
/**
* BST 创建
*/
TreeNode CreateBST(){
final int ENDFLAG = 0;
TreeNode root = new TreeNode();
Scanner input = new Scanner(System.in);
System.out.print("请输入数值:");
int val = input.nextInt();
while(val != ENDFLAG){
InsertBST(root,val);
System.out.print("请输入数值:");
val = input.nextInt();
}
return root;
}
5.二叉排序树的删除
算法步骤:
(1)若要删除结点不在二叉排序树中,则不做任何操作
(2)若要删除结点在二叉排序树中
- 要删除结点的左右子树皆为空,直接删除
- 要删除结点的右子树为空,左子树非空,直接将其左子树作为其父节点的左子树或右子树
- 要删除结点的左子树为空,右子树非空,直接将其右子树作为其父节点的左子树或右子树
- 要删除结点的左子树非空,右子树非空,找到右子树的最左结点并将左子树作为最左结点的左子树,将其右子树作为其父节点的左子树或右子树;另一种方式是在左子树上找到最右结点,代替要删除的结点
算法分析:
二叉排序树删除的基本过程也是查找,所以时间复杂度仍是 。
/**
* BST 删除
*/
void DeleteBST(TreeNode root,int key){
if(root == null){
return;
}
TreeNode cur = root;
TreeNode pre = null;
// 寻找要删除结点
while(cur != null){
if(cur.val == key){
break;
}
pre = cur;
if(cur.val < key){
cur = cur.right;
}else{
cur = cur.left;
}
}
// 要删除结点不在二叉排序树
if(cur == null){
return;
}
// 删除根节点
if(pre == null){
root = deleteOneNode(root);
}
if(pre!=null && pre.left==cur){
pre.left = deleteOneNode(cur);
}
if(pre!=null && pre.right==cur){
pre.right = deleteOneNode(cur);
}
}
TreeNode deleteOneNode(TreeNode root){
if(root == null){
return null;
}
// 情况2
if(root.right == null){
return root.left;
}
// 情况3
if(root.left == null){
return root.right;
}
// 情况4
// 寻找右子树最左结点
TreeNode cur = root.right;
while(cur.left != null){
cur = cur.left;
}
cur.left = root.left;
return root.right;
}
TreeNode deleteOneNode2(TreeNode root){
if(root == null){
return null;
}
// 情况2
if(root.right == null){
return root.left;
}
// 情况3
if(root.left == null){
return root.right;
}
// 情况4
// 寻找左子树最右结点
TreeNode pre = root;
TreeNode cur = root.left;
while(cur.right != null){
pre = cur;
cur = cur.right;
}
// cur 是 root 左子树的最右结点;cur 可能存在左子树
if(pre == root){
pre.left = cur.left;
}else{
pre.right = cur.left;
}
// cur 代替 root
cur.left =root.left;
cur.right = root.right;
return root.right;
}
5.3.2 平衡二叉树
1.平衡二叉树的定义
二叉排序树查找算法的性能取决于二叉树的结构,而二叉排序树的形状则取决于其数据集。如果数据呈有序排列,则二叉排序树是线性的,查找的时间复杂度为 ;反之,如果二叉排序树的结构合理,则查找速度较快,查找的时间复杂度为 。树的高度越小,查找的速度越快。
平衡二叉树,又称为AVL树。
平衡二叉树是具有以下特征的二叉排序树:
(1)左子树和右子树的深度之差的绝对值不超过1
(2)左子树和右子树也是平衡二叉树
二叉树上结点的平衡因子(BF) 定义为该结点左子树和右子树的深度之差。
2.平衡二叉树的平衡调整方法
(1)LL型
由于在A的左子树根节点的左子树上插入结点,使得A的平衡因子由 1 增至 2,需进行一次向右的顺时针旋转。
(2)RR型
由于在A的右子树根节点的右子树上插入结点,使得A的平衡因子由 -1 变为 -2,需进行一次向左的逆时针旋转。
(3)LR型
由于在A的左子树根节点的右子树上插入结点,A的平衡因子由 1 增至2,需进行两次旋转。第一次对B及其右子树进行逆时针旋转(RR型),变成了LL型;第二次进行顺时针旋转。
(4)RL型
由于在A的右子树根节点的左子树上插入结点,A的平衡因子由 -1 变为 -2,需进行两次旋转。第一进行顺时针旋转(LL型),变为RR型;第二次进行逆时针旋转。
// Leetcode:1382. 将二叉搜索树变平衡
public TreeNode balanceBST(TreeNode root) {
if (root == null) {
return null;
}
// node节点的高度缓存
Map<TreeNode, Integer> nodeHeight = new HashMap<>();
TreeNode newRoot = null;
Deque<TreeNode> stack = new LinkedList<>();
TreeNode node = root;
// 先序遍历插入(其实用哪个遍历都行)
while (node != null || !stack.isEmpty()) {
if (node != null) {
// 新树插入
newRoot = insert(newRoot, node.val, nodeHeight);
stack.push(node);
node = node.left;
} else {
node = stack.pop();
node = node.right;
}
}
return newRoot;
}
/**
* 新节点插入
*
* @param root root
* @param val 新加入的值
* @param nodeHeight 节点高度缓存
* @return 新的root节点
*/
private TreeNode insert(TreeNode root, int val, Map<TreeNode, Integer> nodeHeight) {
if (root == null) {
root = new TreeNode(val);
nodeHeight.put(root, 1);// 新节点的高度
return root;
}
TreeNode node = root;
int cmp = val - node.val;
if (cmp < 0) {
// 左子树插入
node.left = insert(root.left, val, nodeHeight);
// 如果左右子树高度差超过1,进行旋转调整
if (nodeHeight.getOrDefault(node.left, 0) -
nodeHeight.getOrDefault(node.right, 0) > 1) {
// LR型
if (val > node.left.val) {
// 插入在左孩子右边,左孩子先左旋
node.left = rotateLeft(node.left, nodeHeight);
}
// LL型
// 节点右旋
node = rotateRight(node, nodeHeight);
}
} else if (cmp > 0) {
// 右子树插入
node.right = insert(root.right, val, nodeHeight);
// 如果左右子树高度差超过1,进行旋转调整
if (nodeHeight.getOrDefault(node.right, 0) -
nodeHeight.getOrDefault(node.left, 0) > 1) {
// RL型
if (val < node.right.val) {
// 插入在右孩子左边,右孩子先右旋
node.right = rotateRight(node.right, nodeHeight);
}
// RR型
// 节点左旋
node = rotateLeft(node, nodeHeight);
}
} else {
// 一样的节点,啥都没发生
return node;
}
// 获取当前节点新高度
int height = getCurNodeNewHeight(node, nodeHeight);
// 更新当前节点高度,每次插入时,都会递归进行更新
nodeHeight.put(node, height);
return node;
}
/**
* node节点左旋,逆时针方向,RR型
* 只需更新 原根节点和新根节点 高度
* @param node node
* @param nodeHeight node高度缓存
* @return 旋转后的当前节点
*/
private TreeNode rotateLeft(TreeNode node, Map<TreeNode, Integer> nodeHeight) {
// ---指针调整
TreeNode right = node.right;
node.right = right.left;
right.left = node;
// ---高度更新
// 先更新node节点的高度,这个时候node是right节点的左孩子
int newNodeHeight = getCurNodeNewHeight(node, nodeHeight);
// 更新node节点高度
nodeHeight.put(node, newNodeHeight);
// newNodeHeight是现在right节点左子树高度,原理一样,取现在right左右子树最大高度+1
int newRightHeight = Math.max(newNodeHeight,
nodeHeight.getOrDefault(right.right, 0)) + 1;
// 更新原right节点高度
nodeHeight.put(right, newRightHeight);
return right;
}
/**
* node节点右旋,顺时针方向,LL型
* 只需更新 原根节点和新根节点 高度
* @param node node
* @param nodeHeight node高度缓存
* @return 旋转后的当前节点
*/
private TreeNode rotateRight(TreeNode node, Map<TreeNode, Integer> nodeHeight) {
// ---指针调整
TreeNode left = node.left;
node.left = left.right;
left.right = node;
// ---高度更新
// 先更新node节点的高度,这个时候node是right节点的左孩子
int newNodeHeight = getCurNodeNewHeight(node, nodeHeight);
// 更新node节点高度
nodeHeight.put(node, newNodeHeight);
// newNodeHeight是现在left节点右子树高度,原理一样,取现在right左右子树最大高度+1
int newLeftHeight = Math.max(newNodeHeight,
nodeHeight.getOrDefault(left.left, 0)) + 1;
// 更新原left节点高度
nodeHeight.put(left, newLeftHeight);
return left;
}
/**
* 获取当前节点的新高度
*
* @param node node
* @param nodeHeight node高度缓存
* @return 当前node的新高度
*/
private int getCurNodeNewHeight(TreeNode node, Map<TreeNode, Integer> nodeHeight) {
// node节点的高度,为现在node左右子树最大高度+1
return Math.max(nodeHeight.getOrDefault(node.left, 0),
nodeHeight.getOrDefault(node.right, 0)) + 1;
}
5.3.3 B-树
前面介绍的查找方法均适用于存储在计算机内存中较小的文件,统称为内查找法。若问价很大且存放于外存进行查找时,这些查找方法就不适用了。
1.B-树的定义
一棵 阶的B-树,或为空树,或为满足下列特性的 叉树:
(1)树中每个结点至多有 棵子树
(2)若根结点不是叶子结点,则至少有两棵子树
(3)除根之外的所有非终端结点至少有 棵子树;关键字不少于
(4)所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点(失败结点并不存在,指向这些结点的指针为空。为了便于分析查找性能)
(5)所有的非终端结点最多有 个关键字,如下图所示:
其中, 为 关键字 ,且 ; 为 指向子树根结点的指针 ; 为 关键字个数 ; 所指子树中所有结点的关键字均小于 ; 所指子树中所有结点的关键字均大于。
2.B-树查找
算法步骤:
(1)若 ,则查找成功
(2)若 ,则顺着指针 所指向的子树继续向下查找
(3)若 ,则顺着指针 所指向的子树继续向下查找
(4)若 ,则顺着指针 所指向的子树继续向下查找
算法分析:
根据B-树定义,第一层至少有 1 个结点;第二层至少有 2 个结点;根据定义中的(3),可知第三层至少有 个结点;...;依次类推,第 层至少有 个结点。
若 阶B-树中具有 个关键字,则叶子结点即查找不成功的结点为 ,由此有:
反之
在含有 个关键字的B-树上进行查找时,从根节点到关键字所在结点的路径上涉及的结点数不超过 。
// 结点内查找关键字
/**
* 采用二分查找法在结点内查找关键字
*/
public SearchResult searchResult(Object key){
int left = 0,right = this.keySize()-1;
boolean isExist = false;
while (left <= right){
int mid = left + (right-left)/2;
Object midValue = this.keyList.get(mid);
int cmp = compare(midValue,key);
if(cmp == 0){
isExist = true;
left = mid;
break;
}else{
// midValue < key
if(cmp < 0){
left = mid + 1;
}else{
right = mid - 1;
}
}
}
if(isExist){
return new SearchResult(this,isExist,left);
}else{
if(right == -1){
// 小于列表中的所有结点
return new SearchResult(this,false,0);
}else if(left == this.keySize()){
// 大于列表中的所有结点
return new SearchResult(this,false,this.keySize());
}
// 例如 P0 1 P1 2 P2 6 P3 7 P4
// 查 5,得到 left=2,right=1 ,应查找 P2
return new SearchResult(this,false,left);
}
}
// 在B-树内查询关键字
/**
* 在以node为根的树内搜索key项
*/
private SearchResult search(Node node,Object key){
SearchResult result = node.searchResult(key);
if(result.isExist()){
return result;
}else{
// 叶子结点
if(node.getLeaf()){
return result;
}
// 递归搜索子结点
int index = result.getIndex();
return search(node.childNodesList.get(index),key);
}
}
3.B-树的插入
算法步骤:
(1)在B-树中查找给定的关键字,若查找成功,则插入操作失败;否则将新记录插入到查询到的叶子结点中(失败结点的上一层)
(2)若插入新记录后,结点中的关键字个数未超过 ,则插入操作成功,否则转入步骤(3)
(3)分裂结点。此时 结点中有 个关键字;令 ,。分为以下三步:
- 分配关键字。新建一个结点 ,将 中的 个结点保存进 ; 只保留 的关键字;
- 分配指针。如果 不是叶子结点,还需要分配指针, 中有 个指针。将 中的 个指针保存进 ; 只保留 个指针;
- 父结点插入 。在父结点上搜索 ,得到索引 ;将 插入 处; 指针已在父结点中,并且明显 指针在其后面,故将 指针插入 处
- 判断父结点中的关键字数是否超过 ;超过,则继续分裂父结点
/**
* 插入新结点
*/
public boolean insertKey(Object key){
// 查询 key 在 B-树 中的情况
SearchResult result = search(root, key);
if(result.isExist()){
return false;
}
return insertKey(result.getNode(),result.getIndex(),key);
}
private boolean insertKey(Node node,int index,Object key){
node.keyList.add(index,key);
if(node.keyList.size() > maxKeySize){
// 分裂
splitNode(node);
}
return true;
}
/**
* 分裂结点
*/
private void splitNode(Node node){
// 取结点中间 key 下标
int midIndex = node.keyList.size() / 2;
// 该 key 需要往上移动
Object key = node.keyList.get(midIndex);
// --------------划分关键字--------------
Node newNode = new Node();
newNode.setLeaf(node.getLeaf());
// 新结点取源结点的(midIndex,node.keyList.size()-1]的关键字
for(int i=midIndex+1;i<node.keyList.size();i++){
newNode.keyList.add(node.keyList.get(i));
}
// 源结点只留下标为[0,midIndex)的关键字
if (node.keyList.size() > midIndex) {
node.keyList.subList(midIndex, node.keyList.size()).clear();
}
// --------------划分指针--------------
// 若不是叶子结点,需要将原来结点的孩子结点也进行分裂,
// 新结点取(midIndex,node.keyList.size()-1]的孩子结点
if (!node.getLeaf()) {
for (int i = midIndex + 1; i < node.childNodesList.size(); i++) {
newNode.childNodesList.add(node.childNodesList.get(i));
// 修改孩子结点的双亲结点为新结点
node.childNodesList.get(i).setParentNode(newNode);
}
// 源结点只留下标为[0,midIndex]的孩子结点
if (node.childNodesList.size() > midIndex + 1) {
node.childNodesList.subList(midIndex + 1,
node.childNodesList.size()).clear();
}
}
// --------------分裂父结点--------------
// 将 key 和 newNode 插入父结点,明显 node 在父结点中已存在,
// 并且 node 代表的数字小于 newNode 代表的数字
Node father = node.getParentNode();
if (null == father) {
// 若结点的双亲结点为null,说明是根结点进行的分裂,需要新增结点作为根结点
father = new Node();
father.childNodesList.add(node);
father.setLeaf(false);
father.keyList.add(key);
father.childNodesList.add(newNode);
node.setParentNode(father);
newNode.setParentNode(father);
} else {
newNode.setParentNode(father);
SearchResult re = father.searchResult(key);
father.keyList.add(re.getIndex(), key);
father.childNodesList.add(re.getIndex() + 1, newNode);
// 若双亲的关键字个数超出最大允许值,则继续分裂
if (father.keyList.size() > maxKeySize) {
splitNode(father);
}
}
if (father.getParentNode() == null) {
this.root = father;
}
}
4.B-树删除
算法步骤:
(1)搜索要删除 所在的结点 ;找不到直接返回
(2)判断 是否为叶子结点;如果不是叶子结点,则找到不大于 的最大关键字 , 所在结点为 ,将 与 进行交换,使 ;然后在叶子结点()上进行删除操作
(3)如果叶子结点中的关键字个数大于 ,则直接将 删除,并返回;否则,进行第(4)步合并
(4)找到 结点的左兄弟 和右兄弟
- 如果 存在,并且关键字个数大于
-
- 获取父结点中关键字 ,该关键字是最大的,并且小于 中所有的关键字
- 将左兄弟的最大关键字插入父结点中 所在的位置,并删除这两个关键字
- 将 插入 结点的第一个位置,并删除
- 如果 存在,并且关键字个数大于
-
- 获取父结点中关键字 ,该关键字是最小的,并且大于 中所有的关键字
- 将右兄弟的最小关键字插入父结点中 所在的位置,并删除这两个关键字
- 将 插入 结点的最后一个位置,并删除
- 左右兄弟结点的关键字个数都不足的话,合并兄弟结点,下放一个双亲结点的关键字
-
- 左兄弟非空,则合并 和 ;将父结点中在这两个指针中间的关键字并入 并删除;将 中所有关键字按顺序并入 ;如果 非叶子结点,则将其指针也按顺序并入;在父结点中删除 指针;用 代表合并后的结果
- 右兄弟非空,执行过程相似,只是要删除的是 ,用 代表合并后的结果
- 如果合并后,其父结点是非终端结点,且关键字个数不足,则对其父结点进行合并
- 如果合并后,其父结点是根节点,并且为空,则用合并后的结点作为根节点
public boolean deleteKey(Object key){
SearchResult result = search(root, key);
if(!result.isExist()){
return false;
}
Node keyInNode = result.getNode();
// 是否是叶子结点
if (!keyInNode.getLeaf()) {
// 非叶子结点,取第i个孩子结点中的最大关键字
Node keyChildNode = keyInNode.childNodesList.get(result.getIndex());
//如果孩子结点不是叶子结点,去找这个子树到叶子结点中最大的关键字,与key互换位置
if (!keyChildNode.getLeaf()) {
keyChildNode = getMaxLeaf(keyChildNode);
}
Object childMinkey = keyChildNode.keyList.get(keyChildNode.keyList.size() - 1);
keyInNode.keyList.add(result.getIndex(), childMinkey);
keyInNode.keyList.remove(key);
keyChildNode.keyList.remove(childMinkey);
keyChildNode.keyList.add(key);
keyInNode = keyChildNode;
}
return deleteKey(keyInNode,key);
}
private boolean deleteKey(Node node,Object key){
// 如果需要删除关键字的结点,原本的关键字个数超过Math.ceil(order / 2.0) - 1
if (node.keyList.size() > nonLeafMinKeys) {
node.keyList.remove(key);
return true;
}
// 如果需要删除关键字的结点,原本的关键字个数不超过Math.ceil(order / 2.0) - 1,则需要进行合并
if (node.keyList.size() == nonLeafMinKeys) {
doManageNode(node, key);
}
return true;
}
private void doManageNode(Node node,Object key){
if(null == node.getParentNode()){
return;
}
// 根据 node 在其父结点中的位置,得到左右兄弟
int nodeIndex = node.getParentNode().childNodesList.indexOf(node);
Node leftNode = null;
Node rightNode = null;
if(0 < nodeIndex){
leftNode = node.getParentNode().childNodesList.get(nodeIndex-1);
}
if(0<=nodeIndex && nodeIndex<node.getParentNode().childNodesList.size()-1){
rightNode = node.getParentNode().childNodesList.get(nodeIndex+1);
}
if(null!=leftNode && leftNode.keyList.size()>nonLeafMinKeys){
// 获取父结点中 nodeIndex-1 处的关键字,该关键字小于 node 中所有的关键字
Object nodeParentKey = node.getParentNode().keyList.get(nodeIndex-1);
// 将左兄弟的最大关键字插入父结点 nodeIndex-1 位置
node.getParentNode().keyList.add(
nodeIndex-1,leftNode.keyList.get(leftNode.keySize()-1));
node.getParentNode().keyList.remove(nodeParentKey);
leftNode.keyList.remove(leftNode.keySize()-1);
// 将 nodeParentKey 插入 node 的第一个位置
node.keyList.add(0,nodeParentKey);
node.keyList.remove(key);
return;
}
if(null!=rightNode && rightNode.keyList.size()>nonLeafMinKeys){
// 获取父结点中 nodeIndex 处的关键字,该关键字大于 node 中所有的关键字
Object nodeParentKey = node.getParentNode().keyList.get(nodeIndex);
// 将右兄弟的最小关键字插入父结点 nodeIndex 位置
node.getParentNode().keyList.add(nodeIndex,rightNode.keyList.get(0));
node.getParentNode().keyList.remove(nodeParentKey);
rightNode.keyList.remove(0);
// 将 nodeParentKey 插入 node 的最后一个位置
node.keyList.add(nodeParentKey);
node.keyList.remove(key);
return;
}
// 左右兄弟结点的关键字个数都不足的话,合并兄弟结点,下放一个双亲结点的关键字
if (leftNode != null) {
//合并结点
node = merge(leftNode, node);
node.keyList.remove(key);
// 父结点是非终端结点,其关键字个数少于 nonLeafMinKeys,则对父结点进行合并
// 如果父结点是根结点,则无需处理
if (node.getParentNode().keyList.size() < nonLeafMinKeys
&& null != node.getParentNode().getParentNode()) {
// 寻找结点合并
doManageNode(node.getParentNode(), null);
}
if (null == node.getParentNode().getParentNode()
&& node.getParentNode().keyList.isEmpty()) {
root = node;
}
return;
}
if (rightNode != null) {
//合并结点
node = merge(node, rightNode);
node.keyList.remove(key);
if (node.getParentNode().keyList.size() < nonLeafMinKeys
&& null != node.getParentNode().getParentNode()) {
// 寻找结点合并
doManageNode(node.getParentNode(), null);
}
if (null == node.getParentNode().getParentNode()
&& node.getParentNode().keyList.isEmpty()) {
root = node;
}
}
}
/**
* 合并两个结点
* 将父结点中的第一个大于所有左结点关键字的关键字并入左结点中,将右结点的关键字也并入左结点中;如果右结点不是叶子结点,将所有指针并入左结点
* 删除父结点中的关键字以及右指针
*/
private Node merge(Node leftNode, Node rightNode) {
int index = leftNode.getParentNode().childNodesList.indexOf(leftNode);
leftNode.keyList.add(leftNode.getParentNode().keyList.get(index));
leftNode.getParentNode().keyList.remove(index);
leftNode.keyList.addAll(rightNode.keyList);
if (!rightNode.getLeaf()) {
leftNode.childNodesList.addAll(rightNode.childNodesList);
}
leftNode.getParentNode().childNodesList.remove(rightNode);
return leftNode;
}
/**
* 获取node结点到最右侧子结点的叶子结点
* @param node
* @return
*/
private Node getMaxLeaf(Node node) {
Node keyChildNode = node.childNodesList.get(node.childNodesList.size() - 1);
if (!keyChildNode.getLeaf()) {
getMaxLeaf(keyChildNode);
}
return keyChildNode;
}
5.3.4 B+树
B+树是一种B-树的变形树,更适合用于文件索引系统。
1.B+树和B-树的差异
一棵 阶的B+树和 阶的B-树的差异在于:
(1)有 棵子树的结点中含有 个关键字
(2)所有的叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
(3)所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字
5.4 散列表的查找
散列法查找——通过对元素的关键字值进行某种运算,直接求出元素的地址,即使用关键字到地址的直接转换方法,而不需要反复比较。因此,散列查找法又叫杂凑法或散列法。
5.4.1 基本术语
(1)散列函数和散列地址: 在记录的存储位置 和关键字 之间建立一个确定的对应关系 ,使 ,称这个对应关系 为散列函数, 为散列地址
(2)散列表: 一个有限连续的地址空间,用以存储按散列函数计算得到相应散列地址的数据记录
(3)冲突和同义词: 对不同的关键字可能得到同一散列地址,即 ,,这种现象称为冲突。具有相同函数值的关键字对该散列函数来说称作同义词, 与 互称为同义词
5.4.2 散列函数的构造方法
构造散列函数,通常需要考虑以下因素:
(1)散列表的长度
(2)关键字的长度
(3)关键字的分布情况
(4)计算散列函数所需时间
(5)记录的查找频率
好的散列函数要遵循的原则:
(1)函数计算要简单,每一个关键字只能有一个散列地址与之对应
(2)函数的值域需要在表长的范围内,计算出的散列地址分布应均匀,尽可能减少冲突
散列函数构造方法:
(1)数字分析法
该方法的适用情况:事先必须明确知道所有的关键字每一位上各种数字的分布情况
(2)平方取中法
取关键字平方后的中间几位或其组合作为散列地址
该方法的适用情况:不能事先了解关键字的所有情况,或难于直接从关键字中找到取值较分散的几位
(3)折叠法
将关键字分割成位数相同的几部分(最后一部分位数可不同),然后取这几部分的叠加和(舍去进位)作为散列地址。有两种方法:移位叠加和边界叠加
该方法适用情况:适合于散列地址的位数较少,而关键字的位数较多,且难于直接从关键字中找到取值较分散的几位
(4)除留余数法
假设散列表表长为 ,选择一个不大于 的数 ,用 去除关键字,除后所得余数为散列地址,即:
5.4.3 处理冲突的方法
1.开放地址法
基本思想是: 把记录都存储在散列表数组中,当某一记录关键字 的初始散列地址为 发生冲突时,以 为基础,采取合适方法计算得到另一个地址 ;若 仍冲突,则以 为基础计算得到 ;依次类推,直到不冲突为止。
(1)线性探测法
从冲突位置的下一个位置开始寻找空的位置,如果到了表尾,则从表头开始再继续寻找。
优点是:只要散列表未满,总能找到一个不发生冲突的位置;缺点是会产生二次聚集现象
(2)二次探测法
优点是:可以避免二次聚集现象;缺点是不能保证一定能找到不发生冲突的地址
(3)伪随机探测法
优点是:可以避免二次聚集现象;缺点是不能保证一定能找到不发生冲突的地址
当表中 位置上都已填有记录时,下一个散列地址为 的记录都将填入有 位置,这种在处理冲突过程中发生的两个第一个散列地址不同的记录争夺同一个后继散列地址的现象称作“二次聚集”(或称作“堆积”)。即在解决同类词的过程中又添加了非同类词的冲突。
2.链地址法
基本思想是: 把具有相同散列地址的记录放在同一个单链表中,称为同义词链表。
5.4.4 散列表的查找
算法分析:
(1)由于冲突的存在,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程,仍需以平均查找长度作为衡量散列表查找效率的量度。公式如下所示:
其中, 为散列表中记录个数, 为成功查找第 个记录所需的比较次数
其中, 为散列函数取值个数, 为散列函数取值为 时查找失败的比较次数(若此时散列地址为 时为空,则只需比较 次;若非空,则需要不断向后比较,直到为空)
(2)查找过程中进行比较的次数取决于三个因素:散列函数、处理冲突的方法和装填因子
越小发生冲突的可能性越小; 越大发生冲突的可能性越大,查询过程中,比较的次数也越多。
(3)散列函数的好坏首先影响出现冲突的频繁程度
public class HashTable {
private final int NULLKEY = Integer.MIN_VALUE;
// 开放地址法
int[] enums;
int length;
public HashTable(int length) {
enums = new int[length];
Arrays.fill(enums,NULLKEY);
this.length = length;
}
/**
* 散列函数
*/
public int hash(int val){
return val%length;
}
/**
* 寻找空地址
*/
public int getNULLAddress(int val){
int index = hash(val);
if(enums[index] == NULLKEY){
return index;
}else if(enums[index] == val){
return -1;
}else{
// 线性探测法
for(int i=1;i<length;i++){
int hi = hash(index+i);
if(enums[hi] == NULLKEY){
return hi;
}
}
}
return -1;
}
/**
* 插入
*/
boolean insert(int val){
int index = getNULLAddress(val);
if(index == -1){
return false;
}
enums[index] = val;
return true;
}
/**
* 搜索
*/
int select(int val){
int h0 = hash(val);
if(enums[h0] == NULLKEY){
return -1;
}else if(enums[h0] == val){
return h0;
}else{
for(int i=1;i<length;i++){
int hi = hash(h0+i);
if(enums[hi] == NULLKEY){
return -1;
}else if(enums[hi] == val){
return hi;
}
}
}
return -1;
}
}
5.5 前缀树
public class TrieMap<V> {
// ASCII 码个数
private static final int R = 256;
// 当前存在 Map 中的键值对个数
private int size = 0;
private static class TrieNode<V> {
V val = null;
TrieNode<V>[] children = new TrieNode[R];
}
// Trie 树的根节点
private TrieNode<V> root = null;
/***** 增/改 *****/
// 在 Map 中添加 key
public void put(String key, V val){
if(!containsKey(key)){
size++;
}
root = put(root,key,val,0);
}
private TrieNode<V> put(TrieNode<V> node,String key,V val,int i){
if(node == null){
node = new TrieNode<>();
}
if(i == key.length()){
node.val = val;
return node;
}
char ch = key.charAt(i);
node.children[ch] = put(node.children[ch],key,val,i+1);
return node;
}
/***** 删 *****/
// 删除键 key 以及对应的值
public void remove(String key){
if(!containsKey(key)){
return;
}
root = remove(root,key,0);
size--;
}
private TrieNode<V> remove(TrieNode<V> node,String key,int i){
if(node == null){
return null;
}
// 删除
if(i == key.length()){
node.val = null;
}else{
char ch = key.charAt(i);
node.children[ch] = remove(node.children[ch],key,i+1);
}
if(node.val != null){
// 如果该 TireNode 存储着 val,不需要被清理
return node;
}
// 检查该 TrieNode 是否还有后缀
for(int j=0;j<R;j++){
if(node.children[j] != null){
// 只要存在一个子节点(后缀树枝),就不需要被清理
return node;
}
}
return null;
}
/***** 查 *****/
// 搜索 key 对应的值,不存在则返回 null
// get("the") -> 4
// get("tha") -> null
public V get(String key){
TrieNode<V> x = getNode(root,key);
if(x==null || x.val==null){
return null;
}
return x.val;
}
// 判断 key 是否存在在 Map 中
// containsKey("tea") -> false
// containsKey("team") -> true
public boolean containsKey(String key){
return get(key) != null;
}
// 在 Map 的所有键中搜索 query 的最短前缀
// shortestPrefixOf("themxyz") -> "the"
public String shortestPrefixOf(String query){
TrieNode<V> p = root;
for(int i=0;i<query.length();i++){
if(p == null){
return "";
}
if(p.val != null){
return query.substring(0,i);
}
char ch = query.charAt(i);
p = p.children[ch];
}
if(p!=null && p.val!=null){
return query;
}
return "";
}
// 在 Map 的所有键中搜索 query 的最长前缀
// longestPrefixOf("themxyz") -> "them"
public String longestPrefixOf(String query){
TrieNode<V> p = root;
int maxLen = 0;
for(int i=0;i<query.length();i++){
if(p == null){
break;
}
if(p.val != null){
maxLen = i;
}
char ch = query.charAt(i);
p = p.children[ch];
}
if(p!=null && p.val!=null){
return query;
}
return query.substring(0,maxLen);
}
// 搜索所有前缀为 prefix 的键
// keysWithPrefix("th") -> ["that", "the", "them"]
public List<String> keysWithPrefix(String prefix){
List<String> ans = new LinkedList<>();
TrieNode<V> x = getNode(root,prefix);
if(x == null){
return ans;
}
traverse(x,new StringBuilder(prefix),ans);
return ans;
}
// 通过回溯遍历多叉树
private void traverse(TrieNode<V> node,StringBuilder path,List<String> ans){
if(node == null){
return;
}
if(node.val != null){
ans.add(path.toString());
}
for(int i=0;i<R;i++){
path.append(i);
traverse(node.children[i],path,ans);
path.deleteCharAt(path.length()-1);
}
}
// 判断是和否存在前缀为 prefix 的键
// hasKeyWithPrefix("tha") -> true
// hasKeyWithPrefix("apple") -> false
public boolean hasKeyWithPrefix(String prefix){
return getNode(root,prefix) != null;
}
// 通配符 . 匹配任意字符,搜索所有匹配的键
// keysWithPattern("t.a.") -> ["team", "that"]
public List<String> keysWithPattern(String pattern){
List<String> ans = new LinkedList<>();
traverse(root,new StringBuilder(),pattern,0,ans);
return ans;
}
private void traverse(TrieNode<V> node,StringBuilder path,String pattern,int i,List<String> ans){
if(node == null){
return;
}
if(i == pattern.length()){
if(node.val != null){
ans.add(path.toString());
}
return;
}
char ch = pattern.charAt(i);
if(ch == '.'){
for(int j=0;j<R;j++){
path.append(j);
traverse(node.children[j],path,pattern,i+1,ans);
path.deleteCharAt(path.length()-1);
}
}else{
path.append(ch);
traverse(node.children[ch],path,pattern,i+1,ans);
path.deleteCharAt(path.length()-1);
}
}
// 通配符 . 匹配任意字符,判断是否存在匹配的键
// hasKeyWithPattern(".ip") -> true
// hasKeyWithPattern(".i") -> false
public boolean hasKeyWithPattern(String pattern){
return hasKeyWithPattern(root,pattern,0);
}
private boolean hasKeyWithPattern(TrieNode<V> node,String pattern,int i){
if(node == null){
return false;
}
if(i == pattern.length()){
return node.val != null;
}
char ch = pattern.charAt(i);
if(ch != '.'){
return hasKeyWithPattern(node.children[ch],pattern,i+1);
}
for(int j=0;j<R;j++){
if(hasKeyWithPattern(node.children[j],pattern,i+1)){
return true;
}
}
return false;
}
// 返回 Map 中键值对的数量
public int size(){
return size;
}
// 从节点 node 开始搜索 key,如果存在返回对应节点,否则返回 null
private TrieNode<V> getNode(TrieNode<V> node, String key) {
TrieNode<V> p = node;
for(int i=0;i<key.length();i++){
if(p == null){
return null;
}
char ch = key.charAt(i);
p = p.children[ch];
}
return p;
}
}
public class TrieSet {
// 底层用一个 TrieMap,键就是 TrieSet,值仅仅起到占位的作用
// 值的类型可以随便设置,我参考 Java 标准库设置成 Object
private final TrieMap<Object> map = new TrieMap<>();
/***** 增 *****/
// 在集合中添加元素 key
public void add(String key) {
map.put(key, new Object());
}
/***** 删 *****/
// 从集合中删除元素 key
public void remove(String key) {
map.remove(key);
}
/***** 查 *****/
// 判断元素 key 是否存在集合中
public boolean contains(String key) {
return map.containsKey(key);
}
// 在集合中寻找 query 的最短前缀
public String shortestPrefixOf(String query) {
return map.shortestPrefixOf(query);
}
// 在集合中寻找 query 的最长前缀
public String longestPrefixOf(String query) {
return map.longestPrefixOf(query);
}
// 在集合中搜索前缀为 prefix 的所有元素
public List<String> keysWithPrefix(String prefix) {
return map.keysWithPrefix(prefix);
}
// 判断集合中是否存在前缀为 prefix 的元素
public boolean hasKeyWithPrefix(String prefix) {
return map.hasKeyWithPrefix(prefix);
}
// 通配符 . 匹配任意字符,返回集合中匹配 pattern 的所有元素
public List<String> keysWithPattern(String pattern) {
return map.keysWithPattern(pattern);
}
// 通配符 . 匹配任意字符,判断集合中是否存在匹配 pattern 的元素
public boolean hasKeyWithPattern(String pattern) {
return map.hasKeyWithPattern(pattern);
}
// 返回集合中元素的个数
public int size() {
return map.size();
}
}