检索算法和技术的本质回顾

目录

一、数据结构和存储特点对检索效率的重大影响总结

二、数组和链表的线性结构检索

(一)基本分析

(二)使用二分查找提升数组检索效率

(三)灵活改造链表提升检索效率

问题背景

解决方案

歌曲块链表的设计

基本设计

检索操作

具体代码实现验证

三、树和跳表非线性结构检索

(一)基本分析

树(通常是平衡二叉搜索树)

跳表

总结

(二)树结构如何进行二分查找

理论基础

代码验证

(三)二叉检索树的检索空间平衡方案

平衡二叉搜索树(AVL树)

红黑树

伸展树

平衡方案说明

(四)跳表如何进行二分查找

基本思想回顾

重新回忆下删除和插入操作

简单代码展示

四、哈希检索

(一)基本的常识分析

基本原理

特点和优点

注意事项和限制

(二)扩展知识分析

Java 8后的HashMap优化:链表转换为红黑树+红黑树退化为链表

链表转换为红黑树

红黑树退化为链表

(三)哈希表缺点分析

(四)哈希函数的应用举例

五、状态检索

(一)基本说明

常见应用场景和简单实现

(二)位图(Bitmap)和布隆过滤器(Bloom Filter)

位图(Bitmap)

布隆过滤器(Bloom Filter)

选择位图或布隆过滤器

(三)扩展:布隆过滤器误判率举例分析

六、倒排索引

(一)正排索引理解

(二)倒排索引理解

(三)如何创建倒排索索引举例

(四)查询倒排索索引举例分析


检索的核心思路,其实就是通过合理组织数据,尽可能地快速减少查询范围。也就是说到更多的检索算法和技术,其实它们的本质都是通过灵活应用各种数据结构的特点来组织数据,从而达到快速减少查询范围的目的。

以下主要内容主要针对其涉及的基本技术进行回顾总结。

一、数据结构和存储特点对检索效率的重大影响总结

检索是一种从存储数据的地方高效地获取所需信息的技术检索效率与数据存储方式之间存在紧密联系,而研究不同数据结构的存储特点对检索效率的影响非常重要。

  1. 数据结构选择:不同的数据结构适用于不同的数据存储和检索需求。例如,哈希表适用于快速查找,但不适合范围查询。树结构(如二叉树或B树)适用于范围查询,但可能不如哈希表在单一查找上效率高。因此,了解不同数据结构的特点以及何时使用它们是至关重要的。

  2. 索引结构:在数据库和搜索引擎中,索引结构用于加速数据的检索。不同的索引结构,如倒排索引、B树索引、散列索引等,适用于不同类型的查询和数据。选择正确的索引结构可以显著提高检索效率。

  3. 数据编码和压缩:数据在存储时可以采用不同的编码和压缩技术。这些技术可以减少存储空间,并在一定程度上影响检索速度。了解如何选择和应用数据编码和压缩技术对于优化存储和检索效率至关重要。

  4. 分布式存储:在大规模系统中,数据通常分布在多个节点上。了解分布式存储的原理以及如何有效地检索分布式数据对于构建高性能系统非常重要。

总之,数据结构和存储特点对检索效率产生重大影响,因此,深入了解这些概念和技术对于设计和优化存储和检索系统至关重要。在不同的应用场景中,选择合适的数据结构和存储方式可以显著提高系统的性能和效率。

二、数组和链表的线性结构检索

(一)基本分析

数组和链表是两种不同的线性数据结构,它们的检索效率在某些方面有所不同,取决于具体的操作和使用情境。

数组的检索效率

  • 随机访问效率高:数组在内存中是连续存储的,这使得随机访问数组中的元素非常高效。只需知道索引即可直接访问该位置的元素,时间复杂度为O(1)。
  • 插入和删除效率低:如果要在数组中插入或删除元素,通常需要将后续元素移动以保持连续性。这样的操作平均时间复杂度为O(n),其中n是数组中的元素数量。

链表的检索效率

  • 随机访问效率低:链表的元素不是连续存储的,因此要访问链表中的某个元素,必须从头节点或其他已知位置开始遍历链表。因此,随机访问的平均时间复杂度为O(n),其中n是链表的长度。
  • 插入和删除效率高:链表在插入和删除元素时通常非常高效。只需修改节点的指针,而不需要移动元素。这些操作的平均时间复杂度为O(1),前提是可以直接访问要插入或删除的节点。

也就是说,如果需要频繁进行随机访问操作,数组通常更有效率。但如果需要频繁进行插入和删除操作,并且对访问效率要求不那么高,链表可能更合适。

在实际应用中,通常会根据具体的操作需求来选择适当的数据结构,或者在需要时考虑使用更高级的数据结构来平衡这些操作的性能。例如,平衡二叉搜索树可以提供较好的插入、删除和查找效率。

(二)使用二分查找提升数组检索效率

二分查找是一种高效的搜索算法,特别适用于已排序的数组。它的基本思想是不断将待查找区间缩小为原来的一半,直到找到目标值或确定目标值不存在为止。下面是一般的二分查找的代码实现及说明:

package org.zyf.javabasic.test.search;/*** @program: zyfboot-javabasic* @description: 一般的二分查找的代码实现* @author: zhangyanfeng* @create: 2024-04-14 11:52**/
public class BinarySearchTest {// 二分查找函数public static int binarySearch(int[] arr, int target) {int left = 0;int right = arr.length - 1;// 当左边界小于等于右边界时继续查找while (left <= right) {// 计算中间位置int mid = left + (right - left) / 2;// 如果目标值等于中间值,则返回中间索引if (arr[mid] == target) {return mid;}// 如果目标值小于中间值,则在左半边继续查找else if (arr[mid] > target) {right = mid - 1;}// 如果目标值大于中间值,则在右半边继续查找else {left = mid + 1;}}// 若未找到目标值,则返回 -1 表示不存在return -1;}public static void main(String[] args) {int[] arr = {1, 3, 5, 7, 9, 11, 13};int target = 7;int index = binarySearch(arr, target);if (index != -1) {System.out.println("目标值 " + target + " 在数组中的索引为:" + index);} else {System.out.println("目标值 " + target + " 不存在于数组中。");}}
}

这个 binarySearch 函数接受一个已排序的整数数组 arr 和一个目标值 target,返回目标值在数组中的索引,如果目标值不存在则返回 -1。

该算法的时间复杂度为 O(log n),其中 n 是数组的长度。相比于简单的线性搜索,二分查找在大型有序数组中的查找效率更高。

(三)灵活改造链表提升检索效率

学习链表是学习如何组织“非连续存储空间”的数据结构

用简单的改造例子展示如何根据实际需求来设计链表的变种,以提升检索效率。

问题背景

假设您需要设计一个音乐播放列表(或歌曲库),用户可以随机访问歌曲,但您希望尽量减少内存占用。

解决方案

  • 传统链表:传统的单向链表需要为每首歌曲分配一个节点,这会浪费大量内存,因为每个节点还需要存储指向下一个节点的指针。
  • 改进方案:为了减少内存占用并提高检索效率,可以设计一个变种链表,其中每个节点不仅存储歌曲信息,还存储一定数量的歌曲。这种变种链表可以称为“歌曲块链表”(Song Block Linked List)。

歌曲块链表的设计

基本设计
  • 每个节点包含一个小数组(或列表),其中存储一定数量的歌曲。数组的大小可以根据实际需求调整,以权衡内存占用和检索效率。
  • 每个节点还包含指向下一个节点的指针,以便可以遍历整个歌曲块链表。

检索操作
  • 当用户要随机访问某首歌曲时,首先确定它在哪个节点的小数组中。可以使用二分查找等方法快速定位。
  • 一旦找到所在的节点,就可以在节点内的小数组中进行线性搜索,以查找目标歌曲。

这种歌曲块链表的设计允许充分利用链表的非连续存储空间特点,减少了内存占用,同时仍然可以实现较快的歌曲检索操作。这个例子展示了如何根据实际需求,结合链表的核心思想,设计出适用的数据结构,以提高检索效率和节省内存。

具体代码实现验证

我们简单写一下代码验证一下吧,首先定义歌曲块节点如下:

package org.zyf.javabasic.test.search;import java.util.ArrayList;
import java.util.List;/*** @program: zyfboot-javabasic* @description: 歌曲块节点* @author: zhangyanfeng* @create: 2024-04-14 11:56**/
public class SongBlockNode {private List<String> songs;  // 存储歌曲的小数组private SongBlockNode next;  // 指向下一个节点的指针// 构造函数public SongBlockNode() {this.songs = new ArrayList<>();this.next = null;}// 添加歌曲到当前节点的小数组中public void addSong(String song) {songs.add(song);}// 获取当前节点的小数组public List<String> getSongs() {return songs;}// 获取下一个节点public SongBlockNode getNext() {return next;}// 设置下一个节点public void setNext(SongBlockNode next) {this.next = next;}
}

然后实现对应的歌曲块链表如下:

package org.zyf.javabasic.test.search;import java.util.List;/*** @program: zyfboot-javabasic* @description: 歌曲块链表* @author: zhangyanfeng* @create: 2024-04-14 11:56**/
public class SongBlockLinkedList {private SongBlockNode head;  // 头节点// 构造函数public SongBlockLinkedList() {head = null;}// 在链表尾部添加一个节点public void addNode(SongBlockNode newNode) {if (head == null) {head = newNode;} else {SongBlockNode current = head;while (current.getNext() != null) {current = current.getNext();}current.setNext(newNode);}}// 二分查找指定歌曲,返回所在节点的索引public int binarySearchSong(String song) {SongBlockNode current = head;int index = 0;// 遍历链表while (current != null) {List<String> songs = current.getSongs();if (songs.contains(song)) {return index + songs.indexOf(song);}current = current.getNext();index += songs.size();}// 歌曲不存在于链表中return -1;}
}

快速验证代码测试代码如下:

package org.zyf.javabasic.test.search;/*** @program: zyfboot-javabasic* @description: 简单验证* @author: zhangyanfeng* @create: 2024-04-14 11:57**/
public class SongBlockListTest {public static void main(String[] args) {// 创建歌曲块链表SongBlockLinkedList songList = new SongBlockLinkedList();// 添加节点SongBlockNode node1 = new SongBlockNode();node1.addSong("Song1");node1.addSong("Song2");songList.addNode(node1);SongBlockNode node2 = new SongBlockNode();node2.addSong("Song3");node2.addSong("Song4");songList.addNode(node2);// 查找歌曲String targetSong = "Song3";int index = songList.binarySearchSong(targetSong);if (index != -1) {System.out.println("歌曲 " + targetSong + " 的索引为: " + index);} else {System.out.println("歌曲 " + targetSong + " 不存在于歌曲库中。");}}
}

运行验证成功,基本输出如下,符合预期。

三、树和跳表非线性结构检索

(一)基本分析

树和跳表都是非线性数据结构,它们在检索方面具有一定的优势,但在不同情况下可能更适用。以下是对树和跳表在非线性结构检索中的分析:

树(通常是平衡二叉搜索树)

优点适用场景
高效的检索:平衡二叉搜索树(如 AVL 树或红黑树)在数据频繁变化时能够保持树的平衡,因此具有高效的检索性能。平均检索时间复杂度为 O(log n)。适用于需要频繁的插入、删除和检索操作的情况,例如数据库索引和有序集合。
插入和删除:平衡树对插入和删除操作也有较高的效率。对数据的要求较高,需要保持数据的有序性时,平衡树是一个好选择。

跳表

优点适用场景
高效的检索:跳表是一种数据结构,它通过多层索引实现高效的跳跃式检索。平均检索时间复杂度为 O(log n),类似于平衡树。适用于需要高效检索操作,但对插入和删除操作的性能要求相对较低的场景。
简单实现:相对于平衡树,跳表的实现相对简单,并且不需要自动平衡。可用于实现有序集合、高性能的跳跃表索引等。

总结

  • 树和跳表都是非线性数据结构,用于高效检索。它们在平均检索时间复杂度上具有相似的性能。
  • 树适用于需要频繁插入、删除和检索的场景,以及对数据的有序性要求较高的情况。
  • 跳表适用于需要高效检索操作,但对插入和删除操作的性能要求相对较低的情况。跳表的实现较为简单。

在实际应用中,选择树还是跳表取决于具体的需求和性能要求。如果插入和删除操作频繁,而且需要保持数据有序性,那么平衡树可能更适合。如果主要关注高效的检索操作,并且可以容忍较低的插入和删除性能,那么跳表可能是更好的选择

(二)树结构如何进行二分查找

理论基础

树结构(特别是二叉树)是通过二分查找的方式来进行检索操作的。二分查找是一种高效的查找算法,它适用于有序数据集合,例如有序的树结构。

二叉树数据结构中每个节点最多有两个子节点,通常分为左子树和右子树。树中的节点按照某种特定的顺序排列,例如,左子树的节点值小于父节点,右子树的节点值大于父节点(或相反,具体取决于树的性质)。

二分查找

  • 二分查找是一种分而治之的策略,它从树的根节点开始,逐步地将搜索范围减半,直到找到目标元素或确定它不存在。
  • 从根节点开始,比较目标元素与当前节点的值。
  • 如果目标元素小于当前节点的值,继续在左子树中查找,因为左子树的值都小于当前节点。
  • 如果目标元素大于当前节点的值,继续在右子树中查找,因为右子树的值都大于当前节点。
  • 重复这个过程,直到找到目标元素或达到叶子节点,如果仍然没有找到,就说明目标元素不存在于树中。

时间复杂度

  • 二分查找在平衡二叉树(例如AVL树)中的时间复杂度为O(log n),其中n是树中节点的数量。这是一种非常高效的检索算法。

总之,树结构通过利用二分查找的方式,将数据按有序的方式组织,以便高效地进行检索操作。在有序的二叉树中,通过比较目标值和当前节点的值,可以确定搜索方向,并且在每一步中将搜索范围减半,从而实现了快速的查找。这使得二叉树成为一种非常有用的数据结构,用于实现高效的查找和排序操作。

代码验证

package org.zyf.javabasic.test.search;import org.zyf.javabasic.letcode.tree.base.TreeNode;/*** @program: zyfboot-javabasic* @description: 用于表示二叉搜索树(BST)及其基本操作,包括插入和查找* @author: zhangyanfeng* @create: 2024-04-14 12:16**/
public class BinarySearchTree {TreeNode root;// 构造函数public BinarySearchTree() {root = null;}// 插入节点public void insert(int val) {root = insertNode(root, val);}// 辅助函数:插入节点private TreeNode insertNode(TreeNode node, int val) {if (node == null) {return new TreeNode(val);}if (val < node.val) {node.left = insertNode(node.left, val);} else if (val > node.val) {node.right = insertNode(node.right, val);}return node;}// 查找节点public boolean search(int val) {return searchNode(root, val);}// 辅助函数:查找节点private boolean searchNode(TreeNode node, int val) {if (node == null) {return false;}if (val == node.val) {return true;} else if (val < node.val) {return searchNode(node.left, val);} else {return searchNode(node.right, val);}}public static void main(String[] args) {// 创建一个二叉搜索树BinarySearchTree bst = new BinarySearchTree();bst.insert(10);bst.insert(5);bst.insert(15);bst.insert(3);bst.insert(7);bst.insert(12);bst.insert(20);// 查找节点int target = 7;if (bst.search(target)) {System.out.println("节点 " + target + " 存在于二叉搜索树中。");} else {System.out.println("节点 " + target + " 不存在于二叉搜索树中。");}}
}

(三)二叉检索树的检索空间平衡方案

二叉搜索树(Binary Search Tree,BST)的检索性能在很大程度上取决于树的平衡性。如果树的平衡性较好,检索操作的平均时间复杂度将保持在O(log n)级别。然而,如果BST不平衡,最坏情况下,检索操作可能需要O(n)的时间,这会显著降低其性能。

为了保持BST的平衡性,可以采用以下平衡方案:

平衡二叉搜索树(AVL树)

  • AVL树是一种自平衡的BST,它通过在每次插入或删除节点后执行旋转操作来保持平衡。
  • 每个节点都有一个平衡因子,表示其左子树的高度和右子树的高度之差。在插入或删除操作后,平衡因子会被更新,并且根据平衡因子的值,执行单旋转或双旋转来恢复平衡。
  • AVL树的平均检索时间复杂度为O(log n),适用于频繁插入和删除操作的场景。

现在代码练习一下吧,基本实现如下:

package org.zyf.javabasic.test.search;/*** @program: zyfboot-javabasic* @description: 表示 AVL 树(平衡二叉搜索树)及其基本操作,包括插入、删除和查找* @author: zhangyanfeng* @create: 2024-04-14 12:22**/
public class AVLTree {TreeNode root;// 获取节点高度private int height(TreeNode node) {if (node == null) {return 0;}return node.height;}// 更新节点高度private void updateHeight(TreeNode node) {node.height = 1 + Math.max(height(node.left), height(node.right));}// 获取平衡因子private int getBalance(TreeNode node) {if (node == null) {return 0;}return height(node.left) - height(node.right);}// 右旋转private TreeNode rightRotate(TreeNode y) {TreeNode x = y.left;TreeNode T2 = x.right;// 执行旋转x.right = y;y.left = T2;// 更新节点高度updateHeight(y);updateHeight(x);return x;}// 左旋转private TreeNode leftRotate(TreeNode x) {TreeNode y = x.right;TreeNode T2 = y.left;// 执行旋转y.left = x;x.right = T2;// 更新节点高度updateHeight(x);updateHeight(y);return y;}// 插入节点public TreeNode insert(TreeNode node, int val) {if (node == null) {return new TreeNode(val);}if (val < node.val) {node.left = insert(node.left, val);} else if (val > node.val) {node.right = insert(node.right, val);} else {return node; // 重复值不插入}// 更新节点高度updateHeight(node);// 获取平衡因子int balance = getBalance(node);// 左旋转if (balance > 1 && val < node.left.val) {return rightRotate(node);}// 右旋转if (balance < -1 && val > node.right.val) {return leftRotate(node);}// 左右旋转if (balance > 1 && val > node.left.val) {node.left = leftRotate(node.left);return rightRotate(node);}// 右左旋转if (balance < -1 && val < node.right.val) {node.right = rightRotate(node.right);return leftRotate(node);}return node;}// 中序遍历打印树public void inOrderTraversal(TreeNode node) {if (node != null) {inOrderTraversal(node.left);System.out.print(node.val + " ");inOrderTraversal(node.right);}}// 查找节点public boolean search(TreeNode node, int val) {if (node == null) {return false;}if (val == node.val) {return true;} else if (val < node.val) {return search(node.left, val);} else {return search(node.right, val);}}// AVL 树节点class TreeNode {int val;int height;TreeNode left;TreeNode right;// 构造函数public TreeNode(int val) {this.val = val;this.height = 1; // 初始高度为 1left = null;right = null;}}public static void main(String[] args) {AVLTree avlTree = new AVLTree();// 插入节点avlTree.root = avlTree.insert(avlTree.root, 10);avlTree.root = avlTree.insert(avlTree.root, 20);avlTree.root = avlTree.insert(avlTree.root, 30);avlTree.root = avlTree.insert(avlTree.root, 40);avlTree.root = avlTree.insert(avlTree.root, 50);avlTree.root = avlTree.insert(avlTree.root, 25);// 中序遍历打印树System.out.println("AVL 树中序遍历结果:");avlTree.inOrderTraversal(avlTree.root);// 查找节点int target = 30;if (avlTree.search(avlTree.root, target)) {System.out.println("\n节点 " + target + " 存在于 AVL 树中。");} else {System.out.println("\n节点 " + target + " 不存在于 AVL 树中。");}}
}

红黑树

  • 红黑树是另一种自平衡的BST,它通过着色节点并遵循一组规则来保持平衡。
  • 红黑树的平衡性通过节点的颜色和特定的规则来维护。这些规则包括节点的颜色不能相邻,以及从任何节点到其每个叶子的路径包含相同数量的黑色节点。
  • 红黑树的平均检索时间复杂度为O(log n),并且相对于AVL树,它的插入和删除操作可能稍微高效一些。

同样的,现在代码练习一下吧,基本实现如下(这也是一般我们面试时候常考的加分项):

package org.zyf.javabasic.test.search;/*** @program: zyfboot-javabasic* @description: 用于表示红黑树及其基本操作,包括插入、删除和查找* @author: zhangyanfeng* @create: 2024-04-14 12:28**/
public class RedBlackTree {private TreeNode root;private static final boolean RED = true;private static final boolean BLACK = false;// 获取节点的颜色private boolean color(TreeNode node) {return node == null ? BLACK : node.isRed;}// 左旋转private TreeNode leftRotate(TreeNode x) {TreeNode y = x.right;x.right = y.left;if (y.left != null) {y.left.parent = x;}y.parent = x.parent;if (x.parent == null) {root = y;} else if (x == x.parent.left) {x.parent.left = y;} else {x.parent.right = y;}y.left = x;x.parent = y;return y;}// 右旋转private TreeNode rightRotate(TreeNode y) {TreeNode x = y.left;y.left = x.right;if (x.right != null) {x.right.parent = y;}x.parent = y.parent;if (y.parent == null) {root = x;} else if (y == y.parent.right) {y.parent.right = x;} else {y.parent.left = x;}x.right = y;y.parent = x;return x;}// 插入节点public void insert(int val) {TreeNode newNode = new TreeNode(val);root = insertNode(root, newNode);newNode.isRed = RED; // 新插入的节点为红色fixInsertViolation(newNode);}// 辅助函数:插入节点private TreeNode insertNode(TreeNode root, TreeNode newNode) {if (root == null) {return newNode;}if (newNode.val < root.val) {root.left = insertNode(root.left, newNode);root.left.parent = root;} else if (newNode.val > root.val) {root.right = insertNode(root.right, newNode);root.right.parent = root;}return root;}// 插入修复违反红黑树性质的情况private void fixInsertViolation(TreeNode node) {while (node != root && color(node.parent) == RED) {if (node.parent == node.parent.parent.left) {TreeNode uncle = node.parent.parent.right;if (color(uncle) == RED) {// Case 1: 叔叔节点是红色node.parent.isRed = BLACK;uncle.isRed = BLACK;node.parent.parent.isRed = RED;node = node.parent.parent;} else {if (node == node.parent.right) {// Case 2: 叔叔节点是黑色,且当前节点是右子节点node = node.parent;leftRotate(node);}// Case 3: 叔叔节点是黑色,且当前节点是左子节点node.parent.isRed = BLACK;node.parent.parent.isRed = RED;rightRotate(node.parent.parent);}} else {TreeNode uncle = node.parent.parent.left;if (color(uncle) == RED) {// Case 1: 叔叔节点是红色node.parent.isRed = BLACK;uncle.isRed = BLACK;node.parent.parent.isRed = RED;node = node.parent.parent;} else {if (node == node.parent.left) {// Case 2: 叔叔节点是黑色,且当前节点是左子节点node = node.parent;rightRotate(node);}// Case 3: 叔叔节点是黑色,且当前节点是右子节点node.parent.isRed = BLACK;node.parent.parent.isRed = RED;leftRotate(node.parent.parent);}}}root.isRed = BLACK;}// 中序遍历打印树public void inOrderTraversal(TreeNode node) {if (node != null) {inOrderTraversal(node.left);System.out.print(node.val + " ");inOrderTraversal(node.right);}}// 查找节点public boolean search(int val) {return searchNode(root, val);}// 辅助函数:查找节点private boolean searchNode(TreeNode node, int val) {if (node == null) {return false;}if (val == node.val) {return true;} else if (val < node.val) {return searchNode(node.left, val);} else {return searchNode(node.right, val);}}// 红黑树节点class TreeNode {int val;boolean isRed; // true 表示红色,false 表示黑色TreeNode left;TreeNode right;TreeNode parent;// 构造函数public TreeNode(int val) {this.val = val;this.isRed = true; // 新插入的节点默认为红色left = null;right = null;parent = null;}}public static void main(String[] args) {RedBlackTree rbTree = new RedBlackTree();// 插入节点rbTree.insert(10);rbTree.insert(20);rbTree.insert(30);rbTree.insert(40);rbTree.insert(50);rbTree.insert(25);// 中序遍历打印树System.out.println("红黑树中序遍历结果:");rbTree.inOrderTraversal(rbTree.root);// 查找节点int target = 30;if (rbTree.search(target)) {System.out.println("\n节点 " + target + " 存在于红黑树中。");} else {System.out.println("\n节点 " + target + " 不存在于红黑树中。");}}
}

伸展树

  • 伸展树是一种自适应的BST,它在每次检索操作后,通过旋转操作将最近访问的节点移到根节点的位置。这有助于提高最近访问的节点的检索速度。
  • 伸展树的平均检索时间复杂度为O(log n),但它可能在插入和删除操作上具有一些性能开销。

同样的,现在代码练习一下吧,基本实现如下:

package org.zyf.javabasic.test.search;import org.zyf.javabasic.letcode.tree.base.TreeNode;/*** @program: zyfboot-javabasic* @description: 用于表示伸展树(Splay Tree)及其基本操作,包括插入、删除和查找* @author: zhangyanfeng* @create: 2024-04-14 12:31**/
public class SplayTree {private TreeNode root;// 旋转:左旋private TreeNode leftRotate(TreeNode x) {TreeNode y = x.right;x.right = y.left;y.left = x;return y;}// 旋转:右旋private TreeNode rightRotate(TreeNode y) {TreeNode x = y.left;y.left = x.right;x.right = y;return x;}// 伸展:将节点 x 伸展到根节点private TreeNode splay(TreeNode root, int val) {if (root == null || root.val == val) {return root;}if (val < root.val) {// 待查找的值在左子树中if (root.left == null) {return root;}// Zig-Zig (left left)if (val < root.left.val) {root.left.left = splay(root.left.left, val);root = rightRotate(root);}// Zig-Zag (left right)else if (val > root.left.val) {root.left.right = splay(root.left.right, val);if (root.left.right != null) {root.left = leftRotate(root.left);}}if (root.left != null) {root = rightRotate(root);}return (root.left == null) ? root : rightRotate(root);} else {// 待查找的值在右子树中if (root.right == null) {return root;}// Zag-Zag (right right)if (val > root.right.val) {root.right.right = splay(root.right.right, val);root = leftRotate(root);}// Zag-Zig (right left)else if (val < root.right.val) {root.right.left = splay(root.right.left, val);if (root.right.left != null) {root.right = rightRotate(root.right);}}if (root.right != null) {root = leftRotate(root);}return (root.right == null) ? root : leftRotate(root);}}// 插入节点public TreeNode insert(int val) {root = insertNode(root, val);root = splay(root, val);return root;}// 辅助函数:插入节点private TreeNode insertNode(TreeNode root, int val) {if (root == null) {return new TreeNode(val);}if (val < root.val) {root.left = insertNode(root.left, val);} else if (val > root.val) {root.right = insertNode(root.right, val);}return root;}// 查找节点public boolean search(int val) {root = splay(root, val);return root != null && root.val == val;}public static void main(String[] args) {SplayTree splayTree = new SplayTree();// 插入节点splayTree.insert(10);splayTree.insert(20);splayTree.insert(30);splayTree.insert(40);splayTree.insert(50);// 查找节点int target = 30;if (splayTree.search(target)) {System.out.println("节点 " + target + " 存在于伸展树中。");} else {System.out.println("节点 " + target + " 不存在于伸展树中。");}}
}

平衡方案说明

选择适当的平衡方案取决于您的具体需求和性能要求。AVL树和红黑树通常用于需要强制平衡性的场景,而伸展树则适用于需要优化最近访问的节点的检索操作的场景。不同的平衡方案可能具有不同的权衡点,因此在选择时需要考虑您的应用程序的特定需求。

(四)跳表如何进行二分查找

跳表(Skip List)是一种数据结构,它是在有序元素集合上执行高效查找、插入和删除操作的一种方式。

基本思想回顾

跳表的二分查找基于多级索引的思想:

  • 跳表包含多个级别(层),每个级别都是一个有序链表,其中包含部分原始数据元素。底层链表包含所有元素,而上层链表则包含底层链表中的一部分元素。
  • 每个级别的链表都是有序的,这意味着在每个级别上都可以执行二分查找。

查找操作

  • 跳表的查找操作从顶层链表的头部开始,逐级向下移动。在每个级别上,它比较当前节点的值与目标值。
  • 如果当前节点的值小于目标值,它会继续向右移动,直到找到一个大于等于目标值的节点。
  • 如果当前节点的值大于目标值,它会向下移动到下一个级别,然后继续查找。

优势

  • 跳表中的多级索引允许快速地跳过一些元素,从而将搜索范围缩小到一个较小的区域,类似于二分查找。
  • 跳表的平均检索时间复杂度是O(log n),其中n是元素的数量。这使得它在某些情况下比传统链表更高效。

总之,跳表通过多级索引的方式,将数据组织成多个有序链表,从而实现了高效的查找操作,类似于二分查找的思想。跳表的平均检索时间复杂度是O(log n),这使得它在某些情况下是一种高效的数据结构,特别是当需要在有序元素集合上执行频繁的查找操作时。跳表还相对容易实现,并且不需要像平衡树那样复杂的平衡算法,因此它在实际应用中具有一定的优势。

重新回忆下删除和插入操作

跳表的插入和删除操作相对复杂一些,因为它们不仅需要在底层链表上执行插入和删除,还需要维护多级索引的平衡性。

插入操作

  1. 首先,要插入一个新元素,需要找到插入位置。从顶层链表的头部开始,逐级向下移动,直到找到需要插入的位置。

  2. 在找到插入位置后,执行插入操作。这涉及到将新元素插入到底层链表中的适当位置。

  3. 接下来,需要考虑维护多级索引的平衡性。为了保持平衡,可以采取以下步骤:

    a.随机决定新元素是否要升级到较高级别的索引。这可以通过投掷硬币或其他随机方法来完成。如果决定升级,将新元素添加到上一级索引中,并重复此步骤,直到不再升级为止。b.在每个级别上,确保在插入位置的左边和右边都有足够多的元素,以便索引仍然有效。如果某个级别的链表太短,可以在该级别上进行拆分,将新元素添加到合适的位置,然后重新建立索引。
  4. 完成插入操作后,跳表的结构应该仍然是有序的,并且多级索引应该保持平衡。

删除操作

  1. 删除操作也需要首先找到要删除的元素所在的位置。从顶层链表的头部开始,逐级向下移动,直到找到要删除的元素。

  2. 在找到要删除的位置后,执行删除操作。这涉及到从底层链表中删除元素,并且可能需要合并或删除相关的索引。

  3. 同样需要维护多级索引的平衡性。为了保持平衡,可以采取以下步骤:

    在每个级别上,检查是否需要删除级别上的某些元素,以维持索引的平衡。如果某个级别的链表太短,可以将其删除或与下一个级别合并。
  4. 完成删除操作后,跳表的结构应该仍然是有序的,并且多级索引应该保持平衡。

需要注意的是,插入和删除操作的实现可能会涉及到一些细节,例如如何处理重复元素或如何处理插入位置和删除位置的边界情况。维护跳表的平衡性也需要仔细考虑,以确保操作的高效性和正确性。不过总体来说,跳表的插入和删除操作可以通过谨慎地执行上述步骤来实现。

简单代码展示

回到基本实现上,先进行以上的基本理解,然后我们简单实现一版代码如下:

package org.zyf.javabasic.test.search;import java.util.Random;/*** @program: zyfboot-javabasic* @description: 用于表示跳表(Skip List)及其基本操作,包括插入、删除和查找* @author: zhangyanfeng* @create: 2024-04-14 12:43**/
public class SkipList {private static final int MAX_LEVEL = 16; // 最大级别private int level; // 当前最大级别private SkipListNode head; // 头节点private Random rand; // 用于生成随机级别的随机数// 构造函数public SkipList() {this.level = 0;this.head = new SkipListNode(Integer.MIN_VALUE, MAX_LEVEL);this.rand = new Random();}// 生成随机级别private int randomLevel() {int level = 0;while (rand.nextDouble() < 0.5 && level < MAX_LEVEL) {level++;}return level;}// 插入节点public void insert(int val) {int newLevel = randomLevel();SkipListNode newNode = new SkipListNode(val, newLevel);SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];SkipListNode current = head;// 从最高级别开始查找插入位置for (int i = level; i >= 0; i--) {while (current.next[i] != null && current.next[i].val < val) {current = current.next[i];}update[i] = current;}// 更新最大级别if (newLevel > level) {for (int i = level + 1; i <= newLevel; i++) {update[i] = head;}level = newLevel;}// 插入节点for (int i = 0; i <= newLevel; i++) {newNode.next[i] = update[i].next[i];update[i].next[i] = newNode;}}// 删除节点public void delete(int val) {SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];SkipListNode current = head;// 查找待删除节点位置for (int i = level; i >= 0; i--) {while (current.next[i] != null && current.next[i].val < val) {current = current.next[i];}update[i] = current;}// 删除节点if (current.next[0] != null && current.next[0].val == val) {SkipListNode deletedNode = current.next[0];for (int i = 0; i <= level; i++) {if (update[i].next[i] == deletedNode) {update[i].next[i] = deletedNode.next[i];}}// 更新最大级别while (level > 0 && head.next[level] == null) {level--;}}}// 查找节点public boolean search(int val) {SkipListNode current = head;for (int i = level; i >= 0; i--) {while (current.next[i] != null && current.next[i].val < val) {current = current.next[i];}}current = current.next[0];return current != null && current.val == val;}class SkipListNode {int val;SkipListNode[] next;// 构造函数public SkipListNode(int val, int level) {this.val = val;this.next = new SkipListNode[level + 1];}}public static void main(String[] args) {SkipList skipList = new SkipList();// 插入节点skipList.insert(10);skipList.insert(20);skipList.insert(30);skipList.insert(40);skipList.insert(50);// 查找节点int target = 30;if (skipList.search(target)) {System.out.println("节点 " + target + " 存在于跳表中。");} else {System.out.println("节点 " + target + " 不存在于跳表中。");}// 删除节点skipList.delete(30);System.out.println("节点 " + target + "在跳表中删除节点后:");if (skipList.search(target)) {System.out.println("节点 " + target + " 存在于跳表中。");} else {System.out.println("节点 " + target + " 不存在于跳表中。");}}
}

四、哈希检索

哈希检索(Hash-Based Retrieval)是一种高效的数据检索方法,基于哈希函数将关键字映射到特定的数据存储位置。哈希检索通常能够在平均情况下实现O(1)级别的检索效率,使得它成为处理大规模数据集的一种重要技术。

(一)基本的常识分析

基本原理

  1. 哈希函数:哈希检索的核心是哈希函数。哈希函数是一个将输入数据(例如关键字)映射到一个固定大小的哈希码或哈希值的函数。这个哈希码通常用作数据的索引。

  2. 哈希表:哈希表是存储数据的数据结构,通常由一个数组构成,每个数组元素称为桶(bucket)。哈希函数将关键字映射到数组的索引位置,从而确定数据应该存储在哪个桶中。

  3. 解决哈希冲突:由于哈希函数的映射可能导致不同的关键字映射到相同的索引位置,这种情况称为哈希冲突。哈希表需要一种方法来解决冲突,通常有两种主要方法:

  • 链地址法:每个桶中存储一个链表或其他数据结构,用于存储具有相同哈希码的关键字的数据。当发生冲突时,新数据将附加到相应的链表上。
  • 开放寻址法:在发生冲突时,尝试在哈希表中的其他位置寻找可用的桶,直到找到一个可用的位置为止。

特点和优点

  1. 高效的检索:在平均情况下,哈希检索可以实现O(1)级别的检索效率,因为哈希函数直接将关键字映射到数据的存储位置,无需遍历整个数据集。

  2. 适用于大规模数据集:哈希检索适用于大规模数据集,因为它的检索时间不会随着数据规模的增加而线性增长。

  3. 快速插入和删除:哈希表通常支持快速的插入和删除操作,因为它们只需要计算哈希码并将数据存储在特定位置。

注意事项和限制

  1. 哈希函数设计:设计一个好的哈希函数非常重要,它应该能够将不同的关键字均匀地映射到哈希表的不同位置,以减少冲突。

  2. 哈希冲突处理:需要合适的冲突处理策略,例如链地址法或开放寻址法,以确保数据的正确存储和检索。

  3. 空间要求:哈希表通常需要额外的内存来存储桶和哈希码,因此可能需要更多的内存空间。

  4. 不适用于范围查询:哈希检索通常不适用于范围查询,因为它无法轻松找到一个范围内的数据,而需要遍历整个哈希表。

总之,哈希检索是一种高效的数据检索方法,特别适用于需要快速查找单个关键字的场景,如查找具有唯一标识符的数据。但需要注意哈希函数的设计和冲突处理以确保正确性和性能。

(二)扩展知识分析

Java 8后的HashMap优化:链表转换为红黑树+红黑树退化为链表

在Java 8之后,Java中的HashMap实现经历了重大改进,包括在链表长度达到一定阈值时将其转换为红黑树,以及在红黑树节点数量降低到一定阈值时将其退化为链表。这个改进主要涉及两个方面:性能和平衡。

链表转换为红黑树

当HashMap的链表长度达到一定的阈值(默认为8)时,Java 8引入了树化(Treeify)机制,将链表转换为红黑树。这是为了解决在极端情况下,链表可能变得非常长,导致查找、插入和删除的性能下降。树化的过程包括以下步骤:

  1. 当链表长度达到阈值时,将链表转换为一个红黑树。
  2. 如果哈希表的大小小于64,则会在树化之前扩大哈希表的容量,以确保树化后的负载因子在0.75以下。
  3. 树化后,HashMap会使用红黑树的查找算法,将查找、插入和删除操作的性能从O(n)提高到O(log n)。
红黑树退化为链表

为了保持性能和平衡,当红黑树中的节点数量降低到一定阈值(默认为6)时,Java 8引入了退化(Untreeify)机制,将红黑树退化为链表。退化的过程包括以下步骤:

  1. 当红黑树中的节点数量降低到阈值以下时,将红黑树还原为链表。
  2. 如果哈希表的大小小于64,则会在退化之前缩小哈希表的容量,以确保负载因子保持在0.75以下。
  3. 退化后,HashMap会使用链表的查找算法,将查找、插入和删除操作的性能回归到O(n)级别。

这种树化和退化机制的引入,使得Java中的HashMap在不同负载情况下能够保持较好的性能。在大多数情况下,HashMap能够提供O(1)级别的查找、插入和删除操作,但在极端情况下,它会自动调整数据结构以维持性能。这使得HashMap成为了一个高性能的数据结构,适用于各种应用场景。

(三)哈希表缺点分析

  1. 冲突处理:冲突是指两个或多个不同的键被哈希函数映射到了相同的存储位置,这可能会导致性能下降。虽然哈希表有解决冲突的方法,如链地址法或开放寻址法,但在极端情况下,冲突仍然可能发生。

  2. 不支持有序存储:哈希表中的数据是无序的,如果需要对数据进行有序访问或范围查询,哈希表不是最佳选择。有序数组或二叉搜索树等数据结构更适合这些需求。

  3. 不适用于小规模数据集:对于小规模的数据集,哈希表可能因为内存占用较高而不划算。哈希表需要预留一定数量的空间来保持性能,对于小数据集来说,可能会浪费大量的内存。

  4. 哈希函数设计:设计一个好的哈希函数对于哈希表的性能至关重要。一个不好的哈希函数可能导致冲突更频繁,从而降低性能。

  5. 内存占用:为了保持性能,哈希表通常需要具有一定的冗余空间。这意味着在存储相对稀疏的数据时,哈希表可能占用较多的内存。

  6. 不适用于高并发写入场景:在高并发写入场景下,多个线程可能同时尝试修改哈希表,需要采用并发控制机制来保护数据结构的一致性,这会增加复杂性。

总的来说,哈希表在大多数情况下都是高效的数据结构,但它不是适用于所有场景的通用解决方案。在选择数据结构时,需要综合考虑应用的具体需求和性能要求,有时候有序数组、二叉搜索树或其他数据结构可能更适合特定的应用场景。

(四)哈希函数的应用举例

考虑一个具体的业务案例:实现一个简单的电话号码簿(电话簿)。在这个电话簿中,用户可以存储联系人的姓名和电话号码,并且能够通过姓名快速查找相应的电话号码。

我们将使用Java来实现这个电话号码簿,其中哈希表将用于快速查找联系人的电话号码。

package org.zyf.javabasic.test;import java.util.HashMap;/*** @program: zyfboot-javabasic* @description: 实现一个简单的电话号码簿(电话簿)。*               在这个电话簿中,用户可以存储联系人的姓名和电话号码,并且能够通过姓名快速查找相应的电话号码。* @author: zhangyanfeng* @create: 2023-09-24 22:50**/
public class PhoneBook {private HashMap<String, String> phoneNumbers;public PhoneBook() {phoneNumbers = new HashMap<>();}// 添加联系人和电话号码public void addContact(String name, String phoneNumber) {phoneNumbers.put(name, phoneNumber);}// 根据姓名查找电话号码public String findPhoneNumber(String name) {if (phoneNumbers.containsKey(name)) {return phoneNumbers.get(name);} else {return "Contact not found";}}public static void main(String[] args) {// 创建电话号码簿PhoneBook phoneBook = new PhoneBook();// 添加联系人和电话号码phoneBook.addContact("Alice", "123-456-7890");phoneBook.addContact("Bob", "987-654-3210");phoneBook.addContact("Charlie", "555-123-4567");// 查找联系人的电话号码System.out.println("Alice's Phone Number: " + phoneBook.findPhoneNumber("Alice"));System.out.println("Bob's Phone Number: " + phoneBook.findPhoneNumber("Bob"));System.out.println("Eve's Phone Number: " + phoneBook.findPhoneNumber("Eve"));}
}

在这个示例中,我们创建了一个名为PhoneBook的类,它包含一个哈希表phoneNumbers用于存储联系人的姓名和电话号码。addContact方法用于添加联系人和电话号码,findPhoneNumber方法用于根据姓名查找电话号码。

main方法中,我们创建了一个电话号码簿对象,并添加了几个联系人的信息。然后,我们使用findPhoneNumber方法查找联系人的电话号码,并输出结果。

这个示例演示了如何使用Java中的HashMap来实现一个简单的电话号码簿,其中哈希表用于快速查找联系人的电话号码。这是一个实际应用中的哈希检索的简单示例。

五、状态检索

(一)基本说明

状态检索是实际工作中的常见需求,它涉及到判断某个对象或信息是否已存在或已处理。在不同的应用场景中,状态检索可能采用不同的数据结构和算法来实现,具体取决于需求和性能要求。

常见应用场景和简单实现

用户注册

  • 场景:在用户注册流程中,需要快速判断一个用户提供的用户名或邮箱是否已经被注册。
  • 实现:通常可以使用哈希表或数据库中的索引来存储已注册的用户名或邮箱。在用户提交注册请求时,可以通过查找哈希表或数据库索引来判断是否已存在。

网页抓取

  • 场景:在网页抓取系统中,需要判断一个URL是否已经被抓取过,以避免重复抓取。
  • 实现:可以使用布隆过滤器(Bloom Filter)来存储已抓取的URL。布隆过滤器是一种高效的数据结构,用于判断元素是否属于一个集合。在抓取前,可以查询布隆过滤器,如果URL可能已存在,则进行进一步检查;否则,进行抓取操作。

缓存系统

  • 场景:在缓存系统中,需要检查某个数据是否已经缓存,以避免重新计算或查询数据库。
  • 实现:通常使用缓存数据结构(如哈希表或分布式缓存)来存储已缓存的数据。在需要获取数据时,首先在缓存中查找,如果存在则返回,否则再去源数据源获取并缓存。

任务队列

  • 场景:在任务队列中,需要判断某个任务是否已经被处理,以避免重复执行。
  • 实现:可以使用消息队列或分布式任务调度系统来管理任务状态。每个任务被处理后,可以标记为已完成或移出队列,以确保不会重复执行。

文件系统

  • 场景:在文件系统中,需要检查某个文件是否已存在,以避免覆盖或重复创建。
  • 实现:可以使用文件系统的API来查询文件是否存在。一些编程语言和操作系统提供了直接的文件检查方法。

(二)位图(Bitmap)和布隆过滤器(Bloom Filter)

位图(Bitmap)和布隆过滤器(Bloom Filter)是两种高效判断对象是否存在的数据结构,它们在某些状态检索问题中具有独特的优势。

快速判断一个对象是否存在的问题。相比于有序数组、二叉检索树和哈希表这三种方案,位图和布隆过滤器其实更适合解决这类状态检索的问题。这是因为,在不要求 100% 判断正确的情况下,使用位图和布隆过滤器可以达到 O(1) 时间代价的检索效率,同时空间使用率也非常高效。

位图(Bitmap)

原理:位图是一种使用位来表示元素是否存在的数据结构。每个元素都映射到位图中的一个位,如果元素存在,则对应位被设置为1,否则设置为0。

优点

  • 空间高效:位图非常节省空间,因为每个元素只需要一个二进制位来表示。
  • 快速判断:检查元素是否存在的操作是O(1),只需查询相应位的状态即可。
  • 支持高并发:位图适合在多线程或多进程环境中并发访问,因为位操作通常是原子的。

局限性

  • 只能用于有限域:位图适用于元素范围有限的情况,比如整数范围。

布隆过滤器(Bloom Filter)

原理:布隆过滤器是一种概率型数据结构,它使用多个哈希函数将元素映射到一个位数组上,并将对应位置的位设置为1。当查询某个元素是否存在时,需要通过所有哈希函数来检查位数组中的对应位,只有当所有位都为1时,才认为元素可能存在。

优点

  • 空间高效:布隆过滤器也非常节省空间,特别适合大规模数据集。
  • 快速判断:查询操作通常是O(1)的,因为只需要检查位数组中的位状态。
  • 可扩展:可以通过调整位数组大小和哈希函数数量来平衡误判率和空间占用。

局限性

  • 存在误判:由于多个元素可能映射到相同的位,因此布隆过滤器可能会存在一定的误判率。这意味着它在需要百分之百准确性的场景下不适用。

选择位图或布隆过滤器

  • 如果需要百分之百的准确性,不能容忍误判的情况下,应该选择位图。位图适用于有限域的情况,可以确保元素是否存在的准确性。

  • 如果可以容忍一定的误判率,并且需要处理大规模的数据集,同时对空间占用有限制,布隆过滤器可能是更好的选择。它可以提供O(1)时间复杂度的查询,适用于状态检索等应用。

总的来说,位图和布隆过滤器都是高效的数据结构,但适用于不同的场景。在选择时,需要根据应用需求、数据规模和空间限制来权衡使用哪种数据结构。

(三)扩展:布隆过滤器误判率举例分析

背景:假设有一个网站,用户可以在该网站上发布文章。为了防止用户发布重复的文章,网站采用了布隆过滤器来记录已经发布过的文章链接。当用户尝试发布一篇新文章时,系统会先使用布隆过滤器来检查这篇文章的链接是否已经存在,以避免重复发布。

参数

  • 布隆过滤器的位数组大小:1,000,000个位(1百万位)
  • 使用的哈希函数数量:5个
  • 文章链接集合:已发布文章的链接集合

误判率计算

误判率通常由布隆过滤器的设计参数和已存储的数据集决定。在这个示例中,误判率可以通过以下方式来估算:

  1. 计算哈希函数的位数组索引位置:当用户尝试发布一篇新文章时,使用5个不同的哈希函数将文章的链接映射到位数组上的5个不同位置。

  2. 检查位数组上的这5个位置是否都为1。如果其中任何一个位置为0,那么系统会判断这篇文章的链接是新的;否则,如果这5个位置都为1,系统可能会误判这篇文章的链接已经存在。

  3. 误判率估算:误判率的估算通常是通过布隆过滤器的公式来计算的,但在这个示例中,我们简单估算一下。假设已发布的文章链接数量是10,000条,那么每个哈希函数的索引位置约为100,000 / 1,000,000 = 0.1,也就是说每个哈希函数在位数组上平均设置了约10%的位。因此,对于一个新链接,如果5个哈希函数都返回0.1,那么它们都会命中已发布的链接,导致误判。

这个估算是非常简化的,真实的误判率取决于哈希函数的质量、位数组的大小以及已存储的数据集大小等因素。布隆过滤器的误判率通常在可接受范围内,并且可以通过增加位数组大小和哈希函数数量来降低误判率,但这也会增加内存消耗。

六、倒排索引

(一)正排索引理解

正排索引(Forward Index),与倒排索引相对应,是一种数据结构,用于存储文档集合中的每个文档的详细信息。正排索引是一种文档级别的索引,通常包含了每篇文档的所有内容,或者至少包含了文档的主要信息,例如标题、正文、作者、发布日期等。

正排索引的主要特点和作用如下:

  1. 文档级别存储:正排索引将每篇文档的信息以文档为单位存储,每个文档在正排索引中有一个唯一的标识符(通常是文档ID)。

  2. 包含详细信息:正排索引通常包含了文档的全部或部分内容,以便可以根据用户的查询需求来检索和展示文档。这使得正排索引非常适合用于展示搜索结果或文档的详细内容。

  3. 不适合词级别检索:与倒排索引不同,正排索引通常不支持词级别的检索。也就是说,正排索引不能像倒排索引那样快速查找包含特定词汇的文档。

  4. 文档关联信息:正排索引中的每个文档可以关联到其它相关信息,例如作者、发布日期、摘要等,这些信息可以用于显示搜索结果的元数据。

  5. 适合文档级别操作:正排索引适合处理文档级别的操作,例如按文档ID查找、按日期排序、展示文档的内容等。

正排索引和倒排索引通常在搜索引擎和信息检索系统中一起使用。当用户发起查询时,首先使用倒排索引快速找到包含查询词汇的文档ID列表,然后使用正排索引检索这些文档的详细信息,以便展示搜索结果。

总之,正排索引用于存储和检索文档级别的信息,通常包含了文档的全部或部分内容以及与文档相关的元数据,是搜索引擎和信息检索系统中的重要组成部分。

(二)倒排索引理解

倒排索引(Inverted Index)是一种用于快速查找文档、单词或词条出现位置的数据结构,广泛用于搜索引擎和信息检索系统中。它的基本思想是将文档中的内容反向映射到单词或词条上,以便能够快速检索文档包含特定单词或词条的情况。倒排索引包含以下主要组成部分:

  1. 词汇表(Vocabulary):词汇表是所有不重复单词或词条的列表,通常按照字母顺序排序。每个词汇表条目通常会关联到一个或多个文档的位置信息。

  2. 倒排列表(Inverted List):每个词汇表条目都对应一个倒排列表,该列表存储了包含该单词或词条的文档的位置信息。倒排列表通常包括文档ID、位置信息等。

下面是一个简单的示例来说明倒排索引的工作原理,假设有三篇文档包含以下文本:

文档1:"This is a sample document."

文档2:"A sample document is created."

文档3:"Document creation is done."

通过构建倒排索引,我们可以得到以下结果:

  • 词汇表:

    • "A"
    • "This"
    • "a"
    • "created"
    • "creation"
    • "document"
    • "done"
    • "is"
    • "sample"
  • 倒排列表:

    • "A": [2]
    • "This": [1]
    • "a": [1, 2]
    • "created": [2]
    • "creation": [3]
    • "document": [1, 2, 3]
    • "done": [3]
    • "is": [1, 2]
    • "sample": [1, 2]

在上面的示例中,我们可以看到词汇表中的每个词汇对应着包含该词汇的文档的位置信息(文档ID)。通过倒排索引,我们可以轻松地查找包含特定词汇的文档,这大大提高了文本检索的效率。倒排索引在搜索引擎中的应用非常广泛,它允许用户在海量文本数据中快速找到相关信息。

(三)如何创建倒排索索引举例

创建倒排索引是一个关键的信息检索任务,让我们通过一个具体的示例来说明如何创建倒排索引。我们将考虑一个小型文档集合,其中包含三篇文档,然后创建一个简化的倒排索引。

文档集合

  • 文档1: "This is a sample document."
  • 文档2: "A sample document is created."
  • 文档3: "Document creation is done."

步骤

  1. 文本预处理:在预处理步骤中,我们进行以下操作:

    • 分词:将文档拆分为单词,同时去除标点符号和停用词。
    • 转小写:将所有单词转换为小写字母,以保持一致性。
    • 词干化:将单词还原为其基本形式。

    预处理后的文档如下:

    文档1:["sample", "document"] 文档2:["sample", "document", "created"] 文档3:["document", "creation", "done"]

  2. 构建倒排索引

    现在,我们将为每个单词构建倒排索引。倒排索引的数据结构通常包括词汇表和倒排列表。

    • 词汇表:词汇表包含所有不重复的单词,按字母顺序排序。

      词汇表:["a", "creation", "created", "document", "done", "is", "sample", "this"]

    • 倒排列表:对于每个单词,我们创建一个倒排列表,其中包含包含该单词的文档ID列表。

      示例倒排列表:

      • "a": [2]
      • "creation": [3]
      • "created": [2]
      • "document": [1, 2, 3]
      • "done": [3]
      • "is": [1, 2]
      • "sample": [1, 2]
      • "this": [1]
  3. 存储索引

    在实际应用中,倒排索引通常存储在磁盘上,以便随时访问。索引可以根据需要进行优化和压缩。

  4. 搜索

    当用户发起搜索查询时,查询单词会匹配词汇表中的条目。然后,可以检索倒排列表以查找包含查询单词的文档ID列表。这些文档ID可以用于检索和排序搜索结果。

例如,如果用户搜索 "sample document",我们首先查找词汇表中的 "sample" 和 "document" 条目,然后检索它们的倒排列表。在这个示例中,两个单词都在文档1和文档2中出现,因此这两篇文档将作为搜索结果返回。

这只是一个简化的示例,真实的倒排索引可以处理大规模文档集合,并包括更多的优化和数据结构。倒排索引是搜索引擎中的核心组成部分,它允许高效地检索文档和返回相关的搜索结果。

(四)查询倒排索索引举例分析

查询倒排索引是搜索引擎的核心操作之一,让我们通过一个具体的示例来分析如何查询倒排索引。

假设我们有以下的倒排索引:

  • 词汇表:

    • "apple"
    • "banana"
    • "cherry"
    • "date"
  • 倒排列表:

    • "apple": [1, 3, 4]
    • "banana": [2, 3]
    • "cherry": [1, 4]
    • "date": [2]

这个倒排索引表示了四篇文档(文档ID 1, 2, 3, 4)中包含的单词和它们的文档关联。

现在,让我们来查询倒排索引:假设用户发起了查询 "apple banana",希望找到包含这两个单词的文档。

  1. 分词和预处理:对查询进行分词和预处理,得到单词列表 ["apple", "banana"]。

  2. 查询词汇表:查询词汇表,查找每个查询词汇的倒排列表。

    • "apple" 对应的倒排列表是 [1, 3, 4]
    • "banana" 对应的倒排列表是 [2, 3]
  3. 合并倒排列表

    对查询结果进行合并。这里我们要找到同时包含 "apple" 和 "banana" 的文档,因此我们需要找到两个倒排列表的交集。

    • "apple" 的倒排列表:[1, 3, 4]
    • "banana" 的倒排列表:[2, 3]

    交集:[3]

    因此,文档ID 3 包含了同时包含 "apple" 和 "banana" 的文档。

  4. 返回结果

    返回文档ID 3 作为查询结果,指示文档3包含了查询中的两个单词。

这个示例展示了如何查询倒排索引以查找包含多个查询词汇的文档。查询过程包括查询词汇表、合并倒排列表,并返回匹配的文档ID。在实际搜索引擎中,这个过程会针对大规模的文档集合和更复杂的查询进行优化和加速。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/963.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

循序渐进丨使用 Python 向 MogDB 数据库批量操作数据的方法

当我们有时候需要向数据库里批量插入数据&#xff0c;或者批量导出数据时&#xff0c;除了使用传统的gsql copy命令&#xff0c;也可以通过Python的驱动psycopg2进行批量操作。本文介绍了使用psycopg2里的executemany、copy_from、copy_to、copy_expert等方式来批量操作 MogDB …

[C++][算法基础]求a的b次方模p的值(快速幂)

给定 n 组 &#xff0c;对于每组数据&#xff0c;求出 的值。 输入格式 第一行包含整数 n。 接下来 n 行&#xff0c;每行包含三个整数 。 输出格式 对于每组数据&#xff0c;输出一个结果&#xff0c;表示 的值。 每个结果占一行。 数据范围 1≤n≤100000, 1≤≤2 …

移动Web学习09-响应式布局bootstrap案例开发

3、综合案例-AlloyTeam移动全端 准备工作 HTML 结构 <title>腾讯全端</title> <link rel"shortcut icon" href"favicon.ico" type"image/x-icon"> <!-- 层叠性&#xff1a;咱们的css 要 层叠 框架的 --> <link rel&…

匿名函数与gorm中的Transaction事务方法

整理下go中的匿名函数&#xff0c;项目中很多地方都在用。 1、函数类型的变量 Go中&#xff0c;函数也是一种数据类型。定义一个函数&#xff0c;把这个函数赋值给一个变量&#xff0c;这个变量就是函数类型的变量&#xff0c;用这个变量等价于直接调函数&#xff1a; packa…

数字阅览室解决方案

一、方案概述 “数字阅览室”概念一经提出&#xff0c;就得到了广泛的关注&#xff0c;纷纷组织力量进行探讨、研究和开发&#xff0c;进行各种模型的试验。随着数字地球概念、技术、应用领域的发展&#xff0c;数字阅览室已成为数字地球家庭的成员&#xff0c;为信息高速公路…

高频前端面试题汇总之JavaScript篇(上)

一、数据类型 1. JavaScript有哪些数据类型&#xff0c;它们的区别&#xff1f; JavaScript共有八种数据类型&#xff0c;分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。 其中 Symbol 和 BigInt 是ES6 中新增的数据类型&#xff1a; Symbol 代…

如何免费申请长期HTTPS证书?

长期HTTPS证书申请步骤&#xff1a; 第一步&#xff1a;确定证书类型 根据你的网站需求&#xff0c;选一种适合的HTTPS证书。一般有这几种&#xff1a; - 域名型&#xff08;DV&#xff09;证书&#xff1a;最基础&#xff0c;验证你对域名的所有权&#xff0c;适合个人网站或…

构建有序链表,有序链表的归并,反转链表

本次将对于构建有序链表&#xff0c;有序链表的归并&#xff0c;反转链表&#xff0c;进行一一介绍和代码分享。 首先是一些链表中的基本的函数&#xff1a; Node* creatList() {Node* headNode (Node*)malloc(sizeof(Node));assert(headNode);headNode->next NULL;retu…

海信电视:中国游戏的影像“黑神话”

【潮汐商业评论/文】 《西游记》最后一难中&#xff0c;通天河老鼋回唐三藏“何时才能修成正果”&#xff0c;《黑神话&#xff1a;悟空》也曾面临这个拷问&#xff0c;如今海信电视与它正在共同回答这个命题。 自2020年发布预告片震动行业后&#xff0c;这部游戏就承载着太多…

Shader 渐变屏幕

渐变 前置工作&#xff0c;创建缓冲&#xff0c;对顶点着色器传递顶点数据 function main() {var canvas document.getElementById(webgl);var gl getWebGLContext(canvas);if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) returnvar n initVertexBuffers(gl); }fu…

HBM 发展史与前景(持续更新)

主页&#xff1a; 元存储博客 文章目录 前言1. JEDEC 规范2. HBM 发展历程3. HBM 应用场景4. HBM 市场前景5. 发展挑战 翻译自&#xff1a; https://namu.wiki/w/HBM 前言 NVIDIA H2 上的 HBM100e。 1. JEDEC 规范 2. HBM 发展历程 HBM技术曾被视为一种噱头&#xff0c;因为它…

PSAvatar:一种基于点的可变形形状模型,用于3D高斯溅射的实时头部化身创建

PSAvatar: A Point-based Morphable Shape Model for Real-Time Head Avatar Creation with 3D Gaussian Splatting PSAvatar&#xff1a;一种基于点的可变形形状模型&#xff0c;用于3D高斯溅射的实时头部化身创建 Zhongyuan Zhao1,2, Zhenyu Bao1,2, Qing Li1, Guoping Qiu3,…

工时管理软件全攻略,8大关键因素一网打尽!

工时管理往往与项目管理与人力资源结合起来&#xff0c;考察每位员工的绩效指标。可以说&#xff0c;工时管理软件至关重要。什么叫工时管理&#xff1f;考虑到工时管理软件的八个关键要素包含&#xff1a;功能、使用体验、集成能力、扩展性、成本效率、安全隐私、技术支持、用…

javaWeb项目-智慧餐厅点餐管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、JavaScript Java…

【漏洞复现】泛微e-cology ProcessOverRequestByXml接口存在任意文件读取漏洞

漏洞描述 泛微e-cology依托全新的设计理念,全新的管理思想。 为中大型组织创建全新的高效协同办公环境。 智能语音办公,简化软件操作界面。 身份认证、电子签名、电子签章、数据存证让合同全程数字化。泛微e-cology ProcessOverRequestByXml接口存在任意文件读取漏洞 免责声…

虚拟机磁盘剩余空间不足

VMware 弹出提示&#xff1a; 对文件“E:\Virtual Machine\CentOS 7 1810 的克隆 (2)\CentOS 7 1810-cl1.vmdk”的操作失败。 如果该文件位于远程文件系统上&#xff0c;请确保网络连接以及该磁盘所在的服务器正常工作。如果该文件位于可移动介质中&#xff0c;请重新连接该介…

从零自制docker-11-【pivotRoot切换实现文件系统隔离】

文章目录 busyboxdocker run -d busybox topcontainerId(docker ps --filter "ancestorbusybox:latest"|grep -v IMAGE|awk {print $1})docker export -o busybox.tar $containerId or sudo docker export 09bbf421d93f > ./busybox.tar tar -xvf busybox.tar -C …

nvm下载的node没有npm

nvm下载的node没有npm 相信大家最近可能发现自己使用的nvm下载nodejs没有npm了。 会出现这种情况&#xff1a; C:\Users\89121>nvm install 15 Downloading node.js version 15.14.0 (64-bit)... Complete Downloading npm version 7.7.6... Download failed. Rolling Bac…

如何查找一篇英文文献的源代码?(论文中没有源代码链接时)如何查找一篇论文的实现代码从而复现论文?

有两个网址&#xff0c;从这两个网址里面能找到论文相关代码&#xff0c;但不确定是不是人家论文里的源代码&#xff0c;但是根据论文实在找不到的情况下&#xff0c;只能试试这两个网址了 1. https://paperswithcode.com/ 2. https://www.catalyzex.com/

【QT进阶】Qt Web混合编程之CMake VS2019编译并使用QCefView(图文并茂超详细版本)

往期回顾 【QT进阶】Qt Web混合编程之CEF、QCefView简单介绍-CSDN博客 【QT进阶】Qt Web混合编程之VS2019 CEF的编译与使用&#xff08;图文并茂超详细介绍&#xff09;-CSDN博客【QT进阶】Qt Web混合编程之QWebEngineView基本用法-CSDN博客【QT进阶】Qt Web混合编程之VS2019 C…