链表是一种线性数据结构,每个节点包含数据和指向下一个节点的引用(即指针)。
LinkedList
类)LinkedList
是 Java 标准库中的一个双向链表实现。我们将通过一些插入、删除和获取操作来演示其使用。
import java.util.LinkedList;
public class LinkedListExample {
public static void main(String[] args) {
// 创建一个LinkedList
LinkedList<Integer> list = new LinkedList<>();
// 插入操作
list.addFirst(10); // 在头部插入元素
list.addLast(20); // 在尾部插入元素
list.add(1, 15); // 在索引1处插入元素
// 获取元素
System.out.println("头部元素: " + list.getFirst()); // 获取头部元素
System.out.println("尾部元素: " + list.getLast()); // 获取尾部元素
System.out.println("索引1处的元素: " + list.get(1)); // 获取索引1处的元素
// 删除操作
list.removeFirst(); // 删除头部元素
list.removeLast(); // 删除尾部元素
list.remove(0); // 删除索引0处的元素
// 最终链表
System.out.println("最终链表: " + list);
}
}
addFirst()
和 addLast()
的时间复杂度为 O(1)
,因为在双向链表中可以直接操作头部和尾部节点。add(index, element)
的时间复杂度为 O(n)
,因为需要遍历链表找到指定位置。get(index)
的时间复杂度为 O(n)
,因为链表不支持随机访问,必须从头或尾遍历。removeFirst()
和 removeLast()
的时间复杂度为 O(1)
。remove(index)
的时间复杂度为 O(n)
,因为需要遍历链表找到要删除的节点。为了更好地理解链表的操作,我们可以实现一个简单的单向链表(单链表)并进行插入、删除和获取操作。
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
class SinglyLinkedList {
private Node head;
public SinglyLinkedList() {
this.head = null;
}
// 在链表的头部插入元素
public void addFirst(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
}
// 在链表末尾插入元素
public void addLast(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
// 删除头部元素
public void removeFirst() {
if (head != null) {
head = head.next;
}
}
// 获取链表中的元素
public void printList() {
Node current = head;
while (current != null) {
System.out.print(current.data + " ");
current = current.next;
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
SinglyLinkedList list = new SinglyLinkedList();
// 插入操作
list.addFirst(10); // 插入到头部
list.addLast(20); // 插入到尾部
list.addFirst(5); // 再插入到头部
// 输出链表
System.out.print("当前链表: ");
list.printList();
// 删除头部元素
list.removeFirst();
System.out.print("删除头部元素后的链表: ");
list.printList();
}
}
addFirst()
在链表头部插入一个新节点,时间复杂度为 O(1)
。addLast()
需要遍历整个链表找到尾部,再进行插入操作,时间复杂度为 O(n)
。removeFirst()
删除链表头部的节点,时间复杂度为 O(1)
。场景1:频繁的插入和删除操作
在需要频繁进行插入和删除操作的场景中(尤其是在头部或尾部),链表比数组更高效。例如,任务调度系统或 LRU 缓存管理中,链表常用于实现队列或缓存的快速增删操作。
场景2:动态数据的处理
当我们不确定数据集的大小或数据需要频繁扩展时,链表的动态性非常适合。数组大小固定,扩展时需要重新分配内存,而链表无需提前分配存储空间。
场景3:不需要随机访问
链表更适合顺序访问数据的场景,例如流处理或者文件解析。对于需要快速随机访问的数据结构,数组或哈希表可能更合适。
链表是一种线性数据结构,由一组节点组成,每个节点包含两个部分:
链表的特点是元素存储在非连续的内存位置,节点通过指针相互连接。这与数组不同,数组要求内存中的元素连续存储。根据链表的类型,可以是单向链表、双向链表或循环链表。
Java 中的 LinkedList
实现的是双向链表(Doubly Linked List)。
每个节点包含三个部分:
LinkedList
支持从链表的两端进行高效的插入和删除操作,因为每个节点都持有对前后节点的引用。
插入操作(Insert):
O(1)
O(n)
,其中 n
是链表的长度。删除操作(Delete):
O(1)
O(n)
。获取元素(Access):
O(n)
。由于链表没有像数组一样的索引,随机访问的效率较低,获取某个特定位置的元素通常需要线性时间。
链表适用于以下场景:
频繁插入和删除元素的场景:特别是在列表的头部和尾部进行插入或删除操作时,链表非常高效,时间复杂度为 O(1)
,而数组插入或删除时需要移动其他元素,时间复杂度为 O(n)
。
动态大小调整的场景:链表可以根据需求动态增长或收缩,而数组需要提前分配固定大小的内存,如果数组满了,必须创建一个新的更大的数组并复制元素,性能会受到影响。
不需要随机访问的场景:链表更适合顺序访问元素的场景,因为链表的随机访问性能较差(O(n)
),而数组支持 O(1)
的随机访问。
在需要高效插入、删除操作但随机访问要求不高的情况下,链表是合适的选择。
数组是一种固定大小的、存储相同数据类型元素的结构,可以通过索引直接访问每个元素。
public class Main {
public static void main(String[] args) {
int[] arr = new int[3]; // 创建长度为3的数组
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
// 打印数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " "); // 输出: 10 20 30
}
}
}
线性表数据结构是指元素按照线性顺序排列的一类数据结构,常见的线性表包括:
这些数据结构的元素排列顺序是线性的,即每个元素有且仅有一个前驱元素和一个后继元素(栈和队列的特殊情况除外)。
获取元素(Access):
数组支持随机访问,根据索引直接获取元素,时间复杂度为 O(1)
。这是因为数组的元素存储在连续的内存空间,通过索引可以立即定位到元素的位置。
删除元素(Delete):
O(1)
。O(n)
,因为删除后需要将后面的所有元素向前移动以填补空位。i
个元素后,需要将 i+1
到最后的所有元素向前移动一位。Java 中 ArrayList
的默认初始化长度是 10。当你创建一个没有指定初始容量的 ArrayList
时,它会默认为 10。
ArrayList<Integer> list = new ArrayList<>(); // 默认初始容量为10
ArrayList
的扩容策略是按 1.5 倍(即当前容量的 50%)进行扩容。例如:
10 + 10/2 = 15
。15 + 15/2 = 22
。具体的实现逻辑为:
int newCapacity = oldCapacity + (oldCapacity >> 1);
其中 oldCapacity >> 1
表示将容量右移一位,相当于除以 2。
ArrayList
在需要扩容时,底层会调用 System.arraycopy()
方法来将旧数组的内容复制到一个更大的新数组中。
System.arraycopy()
的参数含义如下:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
ArrayList
的扩容过程一般为:
System.arraycopy()
,将旧数组的元素复制到新数组。ArrayList
的底层存储数组。扩容代码示例:
// ArrayList扩容示例
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
private void grow(int minCapacity) {
// 计算扩容后的新容量
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
System.arraycopy
可以避免手动循环复制,提高复制效率。
队列是一种先进先出(FIFO)的数据结构,最常用的操作是入队(enqueue)和出队(dequeue)。
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
// 入队操作
queue.add(10);
queue.add(20);
queue.add(30);
// 出队操作
System.out.println(queue.remove()); // 输出: 10
System.out.println(queue.remove()); // 输出: 20
}
}
单端队列(单向队列):
Queue
接口 及其实现类,如 LinkedList
(也实现了 Queue
接口)、PriorityQueue
等可以用来实现单端队列,通常只能在一端(尾部)插入元素,在另一端(头部)移除元素。双端队列:
Deque
接口 对应双端队列,提供了在两端插入和删除元素的能力。常见的实现类有:
ArrayDeque
:基于数组实现的双端队列。LinkedList
:虽然主要是链表实现,但也实现了 Deque
接口,支持双端队列操作。优先队列(Priority Queue):
PriorityQueue
类实现了优先队列,其内部通常通过 二叉堆(Binary Heap) 实现。PriorityQueue
会维护堆的结构,以保证队首(根节点)始终是最小(或最大)的元素。插入和删除操作的时间复杂度为 O(log n)
。延迟队列(DelayQueue):
BlockingQueue
的一种实现。延迟队列中的元素只有在指定的延迟时间过去后才能从队列中取出。该队列的元素必须实现 Delayed
接口,包含一个 getDelay
方法,用于返回延迟时间。DelayQueue
通过内部的 优先队列(如 PriorityQueue
)实现元素的排序,队列会将延迟时间最短的元素优先排在队首,但只有当其延迟时间到达时才能被取出。二叉堆 是完全二叉树,分为 最大堆(Max Heap) 和 最小堆(Min Heap),它们用于实现优先队列。插入和删除的核心在于维护堆的性质。
插入元素过程:
O(log n)
。弹出元素过程:
O(log n)
。延迟队列 常用于需要延迟处理任务的场景,如:
延迟队列 添加信号量的目的是为了在多线程并发环境下对队列的访问进行协调,确保:
线程同步:延迟队列使用信号量(如内部的 ReentrantLock
)来保证多个线程对队列的并发访问是安全的。例如,当一个线程尝试取出队首元素,而另一个线程尝试插入新元素时,信号量可以防止数据竞争和不一致性。
阻塞特性:在没有可取出元素的情况下,队列会让线程进入阻塞状态,直到有新元素被插入或已有元素的延迟时间到达。信号量可以帮助线程有序地被唤醒,避免不必要的忙等待。
堆栈是一种先进后出(LIFO)的数据结构,常见操作包括压栈(push)和弹栈(pop)。
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
// 压栈操作
stack.push(10);
stack.push(20);
stack.push(30);
// 弹栈操作
System.out.println(stack.pop()); // 输出: 30
System.out.println(stack.pop()); // 输出: 20
}
}
堆栈(Stack)是一种 后进先出(LIFO, Last In First Out) 的数据结构,常用于以下场景:
虽然 Java 的 Stack
类可以实现堆栈功能,但在实际开发中不推荐使用它,原因如下:
Stack
继承自 Vector
:Stack
类是 Java 1.0 时代的实现,它继承了 Vector
,而 Vector
是同步的,这意味着 Stack
所有方法都是线程安全的,但在大多数场景下,线程同步是不必要的,反而会造成性能损失。
性能问题:由于 Stack
是同步的,性能不如 ArrayDeque
或 LinkedList
等没有同步开销的实现。
替代方案:Java 官方推荐使用 ArrayDeque
或 LinkedList
来实现栈,而不是 Stack
类。ArrayDeque
是一个更高效的栈实现,不会有同步开销。
ArrayDeque
是基于 动态数组 实现的双端队列。它内部维护了一个循环数组,通过对数组的大小进行动态调整,支持在头部和尾部进行高效的插入和删除操作。
ArrayDeque
可以灵活调整大小,保证插入和删除操作的性能。ArrayDeque
既可以作为栈,也可以作为队列使用:
作为栈:可以使用 push()
在队列头部添加元素,用 pop()
从队列头部移除元素,遵循 LIFO(后进先出)的原则。
ArrayDeque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 插入元素
stack.push(2);
stack.push(3);
System.out.println(stack.pop()); // 输出 3,弹出栈顶元素
作为队列:可以使用 offer()
在尾部添加元素,用 poll()
从头部移除元素,遵循 FIFO(先进先出)的原则。
ArrayDeque<Integer> queue = new ArrayDeque<>();
queue.offer(1); // 插入元素到队尾
queue.offer(2);
queue.offer(3);
System.out.println(queue.poll()); // 输出 1,取出队头元素
ArrayDeque
使用循环数组管理数据,可以在队列的头尾两端高效地进行插入和删除操作。它不会扩容到无限大,只有在数组容量不足时才会动态扩展。
ArrayDeque
初始化为 2 的 n 次幂长度 的原因主要有以下几点:
便于快速扩容:在数组长度为 2 的 n 次幂时,扩容和索引计算可以使用位运算(如 &
和 >>
操作)来替代常规的取模运算,位运算比取模运算效率更高,提升了性能。
循环数组的高效管理:ArrayDeque
是基于循环数组实现的,它使用一个环形缓冲区存储数据,使用 2 的 n 次幂长度可以简化数组下标的计算。在插入、删除时通过 &(array.length - 1)
实现高效的环形数组索引操作。
减少扩容次数:当数组的大小为 2 的 n 次幂时,扩容所需的时间复杂度较低,并且新的数组大小也更容易管理和对齐,使得内存分配更加高效。
综合来看,初始化为 2 的 n 次幂可以简化循环数组的管理,并通过位运算提高性能。
哈希表是一种通过键值对存储数据的数据结构,通常用作映射(Map)的一部分,可以高效地通过键来查找值。
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, Integer> hashMap = new HashMap<>();
// 插入键值对
hashMap.put("Apple", 1);
hashMap.put("Banana", 2);
hashMap.put("Cherry", 3);
// 通过键查找值
System.out.println(hashMap.get("Banana")); // 输出: 2
}
}
散列表(Hash Table) 是一种用于存储键值对的数据结构,通过哈希函数将键映射到特定的数组位置(称为桶,bucket),从而可以快速地插入、删除和查找元素。它的核心思想是通过哈希函数计算索引,通常能够在 O(1) 时间复杂度内完成基本操作。
散列表的效率很大程度上依赖于哈希函数的质量,即它如何将不同的键映射到不同的数组位置,以及如何处理哈希冲突。
散列表的主要优势在于其高效的查找、插入和删除操作:
O(1)
,远快于 O(log n)
的树结构。O(1)
,适用于频繁查找和更新的场景。常见的使用场景:
散列哈希冲突不可避免,因此必须有冲突解决方法,最常见的两种方法是 拉链法 和 开放寻址法。
O(n)
。// Java中的HashMap使用了拉链法
class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next; // 指向下一个元素
}
// Java中的ConcurrentHashMap使用了开放寻址法(CAS + 链表的组合)
除了拉链法和开放寻址法,其他常见的哈希冲突解决方法还有:
双散列法(Double Hashing):使用第二个哈希函数来计算冲突时的探测步长。与开放寻址法类似,但减少了线性或二次探测中的聚集问题。
index = (hash1(key) + i * hash2(key)) % table_size;
线性探测法(Linear Probing):在发生冲突时,依次检查下一个位置,直到找到空位。线性探测容易导致“堆积”(即多个冲突连续发生),影响性能。
二次探测法(Quadratic Probing):探测步长是冲突次数的平方,减少了线性探测的堆积现象。
index = (hash(key) + i^2) % table_size;
再哈希法(Rehashing):当哈希冲突过多时,重新计算哈希表的大小,并根据新的哈希函数重新分配所有元素。
扩容与缩容:哈希表通常在超过一定负载因子(如 0.75)时,会自动扩容以减少冲突几率。缩容则在负载过低时发生。
在 Java 中,HashMap
是一种非常常用的哈希表实现,Java 中针对哈希冲突的处理主要采用了拉链法和一些优化策略。
拉链法:HashMap
默认使用链表处理哈希冲突。当多个元素映射到同一哈希桶时,这些元素会被存放到一个链表中。
public V put(K key, V value) {
int hash = hash(key);
int index = (n - 1) & hash;
Node<K, V> head = table[index];
// 如果冲突,将新元素加入链表中
if (head != null) {
// 遍历链表,插入节点
} else {
// 创建新的桶
table[index] = new Node<>(hash, key, value, null);
}
}
链表转红黑树:当链表长度超过一定阈值(默认 8)时,HashMap
会将链表转换为 红黑树,以避免链表过长导致的查找性能退化。红黑树的查找性能为 O(log n)
。
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
扩容机制:当 HashMap
的元素超过一定负载因子(默认 0.75)时,HashMap
会自动扩容,将原来的数据重新分配到新的、更大的数组中。扩容会减少冲突,提升性能。
ConcurrentHashMap
使用了分段锁机制和开放寻址法(CAS 操作),允许多线程并发访问。它在实现上采用了链表和红黑树的组合来处理哈希冲突,同时利用开放寻址避免了过度的锁竞争。最大堆是一棵完全二叉树,每个节点的值大于等于其子节点的值。插入和删除操作都保持堆的结构。
Java示例:
import java.util.PriorityQueue;
import java.util.Collections;
public class MaxHeap {
public static void main(String[] args) {
// Java中的优先队列默认是最小堆,需要将其转换为最大堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 插入元素
maxHeap.add(10);
maxHeap.add(20);
maxHeap.add(15);
// 打印最大值
System.out.println("Max Heap Root: " + maxHeap.peek());
// 删除最大值并输出
maxHeap.poll();
System.out.println("After removing root, new Max Heap Root: " + maxHeap.peek());
}
}
最小堆是一棵完全二叉树,每个节点的值小于等于其子节点的值。插入和删除操作也保持堆的结构。
Java示例:
import java.util.PriorityQueue;
public class MinHeap {
public static void main(String[] args) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 插入元素
minHeap.add(10);
minHeap.add(20);
minHeap.add(15);
// 打印最小值
System.out.println("Min Heap Root: " + minHeap.peek());
// 删除最小值并输出
minHeap.poll();
System.out.println("After removing root, new Min Heap Root: " + minHeap.peek());
}
}
堆(Heap) 是一种完全二叉树,用于实现优先队列的数据结构。堆的每个节点都有一个值,并且满足以下性质:
堆的存储方式通常是使用数组,因为堆是完全二叉树,节点的位置可以通过数组下标计算:
i
,则左子节点下标为 2*i + 1
,右子节点下标为 2*i + 2
。堆广泛用于需要动态维护数据序列并按优先级进行快速访问的场景,常见的使用场景有:
O(1)
。O(n log n)
,可以用于排序大规模数据。堆可以通过不同的数据结构实现,常见的实现方式包括:
二叉堆(Binary Heap):
O(log n)
。斐波那契堆(Fibonacci Heap):
O(1)
),适用于需要频繁合并堆的情况,如在 Dijkstra 算法中。它采用了多棵堆树的结构,降低了插入和合并的时间复杂度。配对堆(Pairing Heap):
二项堆(Binomial Heap):
O(log n)
。左偏堆(Leftist Heap):
最小堆(Min Heap) 和 最大堆(Max Heap) 的主要区别在于节点的排列规则:
最小堆:
最大堆:
在实际应用中,最小堆常用于实现优先队列,而最大堆常用于需要快速获取最大值的场景。
是的,斐波那契堆(Fibonacci Heap) 是一种特殊的堆,设计用于使某些操作的时间复杂度更加高效。它主要在图算法(如 Dijkstra 和 Prim 算法)中被应用,因为在这些算法中,频繁的插入和合并堆操作使得斐波那契堆具有优势。
O(1)
,因为只需将新节点插入一个最小树中即可。O(1)
,适合需要频繁合并两个堆的场景。O(log n)
,与二叉堆相同。O(1)
,适用于图算法中的最短路径计算。斐波那契堆在理论上提供了更好的性能,但由于结构复杂,在实际使用中,二叉堆和配对堆更为常见,因为它们的实现更简单,且在大多数实际应用场景中性能差异并不显著。
字典树是一种树状结构,主要用于存储字符串。它的每个节点代表一个字符。
Java示例:
class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEndOfWord;
public TrieNode() {
isEndOfWord = false;
for (int i = 0; i < 26; i++) {
children[i] = null;
}
}
}
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// 插入单词
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEndOfWord = true;
}
// 搜索单词
public boolean search(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
int index = ch - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return node.isEndOfWord;
}
}
public class Main {
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("apple");
System.out.println(trie.search("apple")); // 输出 true
System.out.println(trie.search("app")); // 输出 false
}
}
字典树(Trie),又称为前缀树,是一种用于存储字符串集的数据结构。它将字符串中的公共前缀共享,使得查找、插入操作更加高效。字典树的每个节点通常代表一个字符,路径从根节点到某个节点的顺序字符构成了字符串。
isEnd
字段)。例如,插入单词 apple
和 app
会共享相同的前缀节点 'a' -> 'p' -> 'p'
。
字典树的实现可以使用树的节点结构,其中每个节点表示一个字符,并通过子节点链接构成整个字典树。以下是用 Java 实现字典树的示例代码:
class TrieNode {
TrieNode[] children = new TrieNode[26]; // 26 个字母,假设都是小写字母
boolean isEndOfWord = false; // 判断是否是某个单词的结尾
}
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// 插入单词
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
node.children[index] = new TrieNode();
}
node = node.children[index];
}
node.isEndOfWord = true;
}
// 查找单词是否存在
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return node.isEndOfWord;
}
// 查找是否有前缀
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if (node.children[index] == null) {
return false;
}
node = node.children[index];
}
return true;
}
}
字典树被广泛应用于各种场景,特别是与字符串处理相关的任务:
对于字典树:
O(m)
,其中 m
是单词的长度。O(m)
,其中 m
是所要搜索单词的长度。O(n)
,其中 n
是所有单词总字符数。相比其他字符串数据结构,字典树通过前缀共享降低了存储空间需求,且能够在恒定时间内实现前缀查找。
除了标准的字典树(Trie),还有一些其他基于类似思想的树结构,用于处理不同的需求或优化特定操作。
O(m)
时间复杂度的子串搜索。这些字典树的不同实现方式主要在于如何优化空间和时间复杂度,以及适应不同的应用场景。
二分搜索树是每个节点左子树的值小于节点的值,右子树的值大于节点的值。
Java示例:
class TreeNode {
int val;
TreeNode left, right;
public TreeNode(int val) {
this.val = val;
left = right = null;
}
}
class BinarySearchTree {
TreeNode root;
// 插入节点
public void insert(int key) {
root = insertRec(root, key);
}
private TreeNode insertRec(TreeNode root, int key) {
if (root == null) {
root = new TreeNode(key);
return root;
}
if (key < root.val)
root.left = insertRec(root.left, key);
else if (key > root.val)
root.right = insertRec(root.right, key);
return root;
}
// 中序遍历
public void inorder() {
inorderRec(root);
}
private void inorderRec(TreeNode root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.val + " ");
inorderRec(root.right);
}
}
}
public class Main {
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
bst.insert(50);
bst.insert(30);
bst.insert(20);
bst.insert(40);
bst.insert(70);
bst.insert(60);
bst.insert(80);
bst.inorder(); // 输出:20 30 40 50 60 70 80
}
}
**二叉搜索树(Binary Search Tree, BST)**是一种特殊的二叉树,它满足以下性质:
二叉搜索树的用途:
BST 主要用于动态集合操作,比如查找、插入、删除等。在保持树平衡的情况下,BST 可以高效执行这些操作。
对于高度为 h
的二叉搜索树:
O(h)
,最坏情况下为 O(n)
(树退化成链表时)。O(h)
,最坏情况下为 O(n)
。O(h)
,最坏情况下为 O(n)
。如果二叉搜索树是平衡的(比如红黑树或 AVL 树),则时间复杂度为 O(log n)
。
删除一个节点时,根据节点的子节点情况,分为三种情况:
没有子节点(叶子节点):
有一个子节点:
有两个子节点:
具体步骤:
T
的中序后继节点 S
(右子树中的最小节点)。T
的值替换为 S
的值。S
节点。class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) {
this.val = val;
}
}
public class BinarySearchTree {
TreeNode root;
// 删除节点
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (key < root.val) {
root.left = deleteNode(root.left, key); // 在左子树中递归删除
} else if (key > root.val) {
root.right = deleteNode(root.right, key); // 在右子树中递归删除
} else {
// 找到要删除的节点
if (root.left == null) return root.right; // 只有右子节点或无子节点
if (root.right == null) return root.left; // 只有左子节点
// 有两个子节点,找到右子树的最小值节点
TreeNode minNode = findMin(root.right);
root.val = minNode.val; // 替换当前节点的值为最小值
root.right = deleteNode(root.right, minNode.val); // 删除右子树中的最小节点
}
return root;
}
// 查找最小值节点
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}
这段代码展示了如何删除二叉搜索树中具有双子节点的元素。
在一个典型的二叉搜索树中,每个节点通常包含以下信息:
在某些实现中,节点还可以包含其他信息,如节点的高度或子树的大小(用于保持树的平衡)。
在 Java 8 之后,HashMap
的实现中引入了红黑树,当链表长度超过阈值(默认为 8)时,链表会转换成红黑树,而不是二叉搜索树。这是因为:
红黑树是平衡二叉搜索树:
O(log n)
。而普通的二叉搜索树如果不进行平衡操作,可能会退化为链表,导致最坏情况下的操作复杂度变为 O(n)
。哈希冲突处理:
HashMap
使用红黑树处理哈希冲突时,可以在冲突的情况下仍然保证 O(log n)
的查找、插入和删除时间。避免性能退化:
最常见的平衡二叉树是AVL树和红黑树。AVL树每个节点的左右子树高度差不超过1。
这里以AVL树为例,提供插入操作的示例。
Java示例:
class AVLNode {
int key, height;
AVLNode left, right;
AVLNode(int d) {
key = d;
height = 1;
}
}
class AVLTree {
AVLNode root;
int height(AVLNode N) {
if (N == null)
return 0;
return N.height;
}
// 右旋转
AVLNode rightRotate(AVLNode y) {
AVLNode x = y.left;
AVLNode T2 = x.right;
x.right = y;
y.left = T2;
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
// 左旋转
AVLNode leftRotate(AVLNode x) {
AVLNode y = x.right;
AVLNode T2 = y.left;
y.left = x;
x.right = T2;
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
return y;
}
int getBalance(AVLNode N) {
if (N == null)
return 0;
return height(N.left) - height(N.right);
}
AVLNode insert(AVLNode node, int key) {
if (node == null)
return (new AVLNode(key));
if (key < node.key)
node.left = insert(node.left, key);
else if (key > node.key)
node.right = insert(node.right, key);
else
return node;
node.height = 1 + Math.max(height(node.left), height(node.right));
int balance = getBalance(node);
if (balance > 1 && key < node.left.key)
return rightRotate(node);
if (balance < -1 && key > node.right.key)
return leftRotate(node);
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
public void inorder(AVLNode node) {
if (node != null) {
inorder(node.left);
System.out.print(node.key + " ");
inorder(node.right);
}
}
}
public class Main {
public static void main(String[] args) {
AVLTree tree = new AVLTree();
tree.root = tree.insert(tree.root, 10);
tree.root = tree.insert(tree.root, 20);
tree.root = tree.insert(tree.root, 30);
tree.root = tree.insert(tree.root, 40);
tree.root = tree.insert(tree.root, 50);
tree.root = tree.insert(tree.root, 25);
tree.inorder(tree.root); // 输出:10 20 25 30 40 50
}
}
在 AVL 树中,平衡因子(Balance Factor, BF) 是一个节点的左子树高度减去右子树高度的值,用于判断该节点是否平衡。具体计算方法如下:
平衡因子 = 左子树高度 − 右子树高度 \text{平衡因子} = \text{左子树高度} - \text{右子树高度} 平衡因子=左子树高度−右子树高度
对于 AVL 树中的每个节点,平衡因子的值应当在 -1 到 +1 之间。如果某个节点的平衡因子超出这个范围,就需要进行旋转操作来重新平衡树。
左旋操作 的主要目的是解决因右子树过高而导致的不平衡问题。当一个节点的右子树比左子树高出两层或更多时(即平衡因子为 -2),左旋操作可以将树的右侧“下沉”,将该节点转变为其右子节点的左子节点,从而重新平衡树。
以下是 AVL 树左旋操作的步骤:
x
,其右子节点为 y
。y
的左子树 变为 x
的右子树(y.left
)。x
和 y
的位置:将 y
移到 x
的位置,x
变为 y
的左子节点。x
变为 y
的左子节点,将 y
的左子树设置为 x
的右子树。下面是 Java 代码示例,展示了左旋操作:
class TreeNode {
int val;
TreeNode left, right;
int height;
TreeNode(int val) {
this.val = val;
this.height = 1; // 新节点的高度为 1
}
}
public class AVLTree {
// 左旋操作
private TreeNode leftRotate(TreeNode x) {
TreeNode y = x.right; // 右子节点
TreeNode T2 = y.left; // y 的左子树
// 执行左旋
y.left = x;
x.right = T2;
// 更新高度
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
// 返回新的根节点
return y;
}
private int height(TreeNode node) {
return node == null ? 0 : node.height;
}
}
在 AVL 树中,有四种主要的不平衡情况,通常需要使用双旋转来恢复平衡:
双旋转通常用于处理更复杂的失衡情况,以下是双旋转操作的示例:
// 左-右(LR)旋转
private TreeNode leftRightRotate(TreeNode node) {
node.left = leftRotate(node.left); // 对左子树进行左旋
return rightRotate(node); // 对当前节点进行右旋
}
// 右-左(RL)旋转
private TreeNode rightLeftRotate(TreeNode node) {
node.right = rightRotate(node.right); // 对右子树进行右旋
return leftRotate(node); // 对当前节点进行左旋
}
插入操作:AVL 树的插入操作需要维护树的平衡,因此插入的时间复杂度为 O(log n)
。虽然每次插入后可能需要进行旋转操作,但每次旋转操作的时间复杂度是常数级别,所以整体复杂度仍为 O(log n)
。
读取操作(查找操作):AVL 树的查找操作也需要遵循树的平衡性质,因此读取(查找)的时间复杂度为 O(log n)
。由于 AVL 树保持平衡,所以查找操作的复杂度较低。
AVL 树的这些复杂度保证了其在各种操作中的高效性,特别是在需要频繁插入和查找的应用场景中。
Java使用示例:
import java.util.TreeMap;
public class RedBlackTreeExample {
public static void main(String[] args) {
TreeMap<Integer, String> treeMap = new TreeMap<>();
// 插入元素
treeMap.put(10, "Ten");
treeMap.put(20, "Twenty");
treeMap.put(15, "Fifteen");
// 打印所有键值对
for (Integer key : treeMap.keySet()) {
System.out.println(key + " -> " + treeMap.get(key));
}
// 查找某个键
System.out.println("Value for key 20: " + treeMap.get(20));
// 删除某个键
treeMap.remove(15);
System.out.println("After removal, value for key 15: " + treeMap.get(15));
}
}
TreeMap
内部使用红黑树来保持有序性。
红黑树是一种自平衡的二叉搜索树,其广泛应用于需要高效查找、插入和删除操作的场景。以下是红黑树的典型使用场景:
TreeMap
和 TreeSet
类,红黑树提供了有序的键值存储。红黑树与普通的二叉搜索树(BST)相比,主要有以下优势:
平衡性:
O(log n)
。而普通 BST 可能会退化为链表,导致操作时间复杂度为 O(n)
。更一致的性能:
O(log n)
,而普通 BST 在极端情况下(例如有序数据插入)可能表现为线性时间复杂度。B-树是一种自平衡的树数据结构,广泛用于数据库和文件系统中。它的主要特点是可以有多个子节点,支持高效的查找、插入和删除操作。
B-树的特点:
B-树的变种:
在红黑树中,插入新节点后需要执行一些操作以维护树的性质。以下是不同情况下的处理方式:
染色(重新上色):
左旋:
右旋:
左旋 + 右旋:
情况:在树中出现了左-右(LR)不平衡时,即当前节点的左子节点的右子树过高。
处理:先对左子节点进行左旋,使其变成左-左情况,然后对当前节点进行右旋。
右旋 + 左旋:
情况:在树中出现了右-左(RL)不平衡时,即当前节点的右子节点的左子树过高。
处理:先对右子节点进行右旋,使其变成右-右情况,然后对当前节点进行左旋。
红黑树通过这些规则来保证树的平衡性,从而在插入、删除和查找操作中保持对数级别的时间复杂度。
2-3树是一种自平衡的查找树,其中每个节点可以有两个或三个子节点。
// 这是一个简化的2-3树节点示例,实际实现会复杂得多
class Node {
int[] keys;
Node[] children;
boolean isLeaf;
int numKeys;
Node(int degree, boolean isLeaf) {
keys = new int[degree - 1];
children = new Node[degree];
this.isLeaf = isLeaf;
numKeys = 0;
}
}
class TwoThreeTree {
private Node root;
private int degree;
TwoThreeTree(int degree) {
this.degree = degree;
root = new Node(degree, true);
}
// 插入、删除及搜索操作会更复杂,这里仅展示一个结构的初始化
public void insert(int key) {
// 插入操作的实现将涉及到分裂节点等操作
}
}
public class Main {
public static void main(String[] args) {
TwoThreeTree tree = new TwoThreeTree(3);
tree.insert(10);
tree.insert(20);
tree.insert(15);
// 实际使用时,需要实现遍历和验证树结构
}
}
2-3 树 是一种自平衡的搜索树,每个节点可以有两种形态:
因此,一个节点最多可以存放两个元素。
2-3 树的插入操作通常包括以下步骤:
插入的时间复杂度为 O(log n),因为在最坏情况下需要对树的高度进行调整,而树的高度是对数级别的。
在 2-3 树中,节点的数量分布是有严格限制的。每个节点最多只能有两个元素,因此当一个节点(如 3 节点)被插入新元素后,必须进行分裂以保持树的性质。
处理过程:
例如,假设我们有一个 3 节点([A, B]
),并且我们要插入 C
。节点 [A, B]
会被分裂为两个 2 节点,并且中间的元素 B
将被提升到父节点。
下面是一个简单的 2-3 树的 Java 实现,包括插入操作和节点分裂的处理:
class Node {
int[] keys = new int[2]; // 2-3 节点最多包含两个键
Node[] children = new Node[3]; // 2-3 节点最多有三个子节点
int numKeys = 0; // 当前节点的键数量
// 判断当前节点是否为 2 节点
boolean is2Node() {
return numKeys == 1;
}
// 判断当前节点是否为 3 节点
boolean is3Node() {
return numKeys == 2;
}
}
class TwoThreeTree {
Node root;
// 插入操作
public void insert(int key) {
if (root == null) {
root = new Node();
root.keys[0] = key;
root.numKeys = 1;
} else {
Node newRoot = insert(root, key);
if (newRoot != null) {
root = newRoot; // 新的根节点
}
}
}
private Node insert(Node node, int key) {
if (node.children[0] == null) { // 叶子节点
if (node.is2Node()) {
insertInto2Node(node, key);
return null;
} else { // 当前节点是 3 节点
return splitNode(node, key);
}
} else { // 内部节点
Node child = getChild(node, key);
Node newChild = insert(child, key);
if (newChild != null) {
return splitNode(node, newChild.keys[0], newChild);
}
return null;
}
}
private void insertInto2Node(Node node, int key) {
if (key < node.keys[0]) {
node.keys[1] = node.keys[0];
node.keys[0] = key;
} else {
node.keys[1] = key;
}
node.numKeys = 2;
}
private Node splitNode(Node node, int key) {
Node newNode = new Node();
if (key < node.keys[0]) {
newNode.keys[0] = node.keys[1];
node.numKeys = 1;
newNode.numKeys = 1;
newNode.children[0] = node.children[1];
newNode.children[1] = node.children[2];
node.children[1] = node.children[0];
node.children[2] = null;
node.keys[1] = key;
} else {
newNode.keys[0] = key;
node.numKeys = 1;
newNode.numKeys = 1;
newNode.children[0] = node.children[1];
newNode.children[1] = node.children[2];
node.children[1] = node.children[0];
node.children[2] = null;
}
return newNode;
}
private Node getChild(Node node, int key) {
if (key < node.keys[0]) {
return node.children[0];
} else if (node.numKeys == 1 || key < node.keys[1]) {
return node.children[1];
} else {
return node.children[2];
}
}
private Node splitNode(Node node, int key, Node newChild) {
Node newNode = new Node();
if (key < node.keys[0]) {
newNode.keys[0] = node.keys[1];
newNode.children[0] = node.children[2];
newNode.children[1] = newChild;
node.keys[1] = key;
node.numKeys = 1;
newNode.numKeys = 1;
} else {
newNode.keys[0] = key;
newNode.children[0] = newChild;
newNode.children[1] = node.children[2];
node.numKeys = 1;
newNode.numKeys = 1;
}
return newNode;
}
}
这个简单的实现示例可以帮助理解 2-3 树的基本操作。实际应用中,还需要处理更多细节和特殊情况。
并查集用于处理一些不交集的合并和查找操作,常用于处理动态连通性问题。
Java示例:
class DisjointSet {
private int[] parent;
private int[] rank;
public DisjointSet(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int u) {
if (parent[u] != u) {
parent[u] = find(parent[u]); // 路径压缩
}
return parent[u];
}
public void union(int u, int v) {
int rootU = find(u);
int rootV = find(v);
if (rootU != rootV) {
if (rank[rootU] > rank[rootV]) {
parent[rootV] = rootU;
} else if (rank[rootU] < rank[rootV]) {
parent[rootU] = rootV;
} else {
parent[rootV] = rootU;
rank[rootU]++;
}
}
}
}
public class Main {
public static void main(String[] args) {
DisjointSet ds = new DisjointSet(5);
ds.union(0, 1);
ds.union(1, 2);
ds.union(3, 4);
System.out.println("Find 0: " + ds.find(0)); // 输出:0
System.out.println("Find 2: " + ds.find(2)); // 输出:0
System.out.println("Find 3: " + ds.find(3)); // 输出:3
System.out.println("Find 4: " + ds.find(4)); // 输出:3
}
}
并查集(Disjoint Set Union, DSU)是一种数据结构,用于处理一些不交集(disjoint sets)合并和查询操作。它支持两个主要操作:
并查集的核心在于高效地处理集合的合并和查找操作,它可以通过以下两个主要策略来优化:
并查集广泛应用于处理具有集合集合关系的问题,以下是一些常见的应用场景:
在并查集中,合并两个元素时,实际上是将它们所在的集合合并。基本步骤如下:
并查集的合并操作可以通过以下策略进行优化:
按秩合并(Union by Rank/Size):
路径压缩(Path Compression):
路径压缩是一个在查找操作中优化并查集的方法,通过将路径上的所有节点直接连接到根节点,来减少树的高度。以下是路径压缩的具体实现方法:
路径压缩实现代码示例(Java):
class DisjointSet {
private int[] parent;
private int[] rank;
public DisjointSet(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i; // 初始时每个元素的父节点是自己
rank[i] = 0; // 初始时每个元素的秩为 0
}
}
// 查找操作(带路径压缩)
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并操作(带按秩合并)
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 按秩合并
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
通过路径压缩和按秩合并,并查集能够高效地处理集合的合并和查找操作,并在实际应用中表现出接近常数时间复杂度。
图是一种由节点(顶点)和节点之间的连接(边)组成的结构,广泛用于建模现实世界的关系网络。图可以是有向的或无向的,有权或无权。图的常见操作包括搜索、遍历、路径查找等。
图可以通过邻接矩阵或邻接表表示。
**邻接矩阵:**使用二维数组表示图的连接关系,矩阵中的值表示两个顶点是否有边。
class GraphMatrix {
private int[][] adjMatrix;
private int numVertices;
public GraphMatrix(int numVertices) {
this.numVertices = numVertices;
adjMatrix = new int[numVertices][numVertices];
}
public void addEdge(int i, int j) {
adjMatrix[i][j] = 1; // 无向图可以同时设置 adjMatrix[i][j] 和 adjMatrix[j][i]
}
public void removeEdge(int i, int j) {
adjMatrix[i][j] = 0;
}
public void printGraph() {
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
System.out.print(adjMatrix[i][j] + " ");
}
System.out.println();
}
}
}
邻接表: 使用链表或数组列表来存储每个顶点的邻接节点。
import java.util.LinkedList;
class GraphList {
private LinkedList<Integer>[] adjList;
public GraphList(int numVertices) {
adjList = new LinkedList[numVertices];
for (int i = 0; i < numVertices; i++) {
adjList[i] = new LinkedList<>();
}
}
public void addEdge(int i, int j) {
adjList[i].add(j); // 无向图可以同时加入 adjList[i] 和 adjList[j]
}
public void printGraph() {
for (int i = 0; i < adjList.length; i++) {
System.out.print("Vertex " + i + ": ");
for (Integer neighbor : adjList[i]) {
System.out.print(neighbor + " ");
}
System.out.println();
}
}
}
图的两种主要遍历方式是深度优先搜索(DFS)和广度优先搜索(BFS)。
DFS:
import java.util.*;
class GraphDFS {
private LinkedList<Integer>[] adjList;
public GraphDFS(int numVertices) {
adjList = new LinkedList[numVertices];
for (int i = 0; i < numVertices; i++) {
adjList[i] = new LinkedList<>();
}
}
public void addEdge(int i, int j) {
adjList[i].add(j);
}
public void DFS(int start) {
boolean[] visited = new boolean[adjList.length];
DFSUtil(start, visited);
}
private void DFSUtil(int v, boolean[] visited) {
visited[v] = true;
System.out.print(v + " ");
for (int neighbor : adjList[v]) {
if (!visited[neighbor]) {
DFSUtil(neighbor, visited);
}
}
}
}
BFS:
import java.util.*;
class GraphBFS {
private LinkedList<Integer>[] adjList;
public GraphBFS(int numVertices) {
adjList = new LinkedList[numVertices];
for (int i = 0; i < numVertices; i++) {
adjList[i] = new LinkedList<>();
}
}
public void addEdge(int i, int j) {
adjList[i].add(j);
}
public void BFS(int start) {
boolean[] visited = new boolean[adjList.length];
Queue<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.add(start);
while (!queue.isEmpty()) {
int v = queue.poll();
System.out.print(v + " ");
for (int neighbor : adjList[v]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.add(neighbor);
}
}
}
}
}
图 是一种用于表示对象及其之间关系的数据结构。图在很多实际应用中都有重要作用,以下是一些常见的使用场景:
图的分类可以基于多个标准进行:
按边的方向:
按权重:
按图的连通性:
按是否有环:
特殊图:
在加权图中,边的权重可以通过以下两种主要方式存储:
邻接矩阵(Adjacency Matrix):
matrix[i][j]
表示从节点 i
到节点 j
的边的权重。如果是无向图,则矩阵是对称的。对于无权图,矩阵中可以用 1
或 0
表示边的存在与否。int[][] graph = new int[V][V];
邻接表(Adjacency List):
import java.util.*;
class Graph {
private final int V;
private final LinkedList<Edge>[] adjList;
class Edge {
int dest;
int weight;
Edge(int dest, int weight) {
this.dest = dest;
this.weight = weight;
}
}
Graph(int V) {
this.V = V;
adjList = new LinkedList[V];
for (int i = 0; i < V; i++) {
adjList[i] = new LinkedList<>();
}
}
void addEdge(int src, int dest, int weight) {
adjList[src].add(new Edge(dest, weight));
// For undirected graph:
adjList[dest].add(new Edge(src, weight));
}
}
广度遍历(BFS, Breadth-First Search)是一种图遍历算法,用于逐层访问节点。BFS 的主要思想是从起始节点开始,访问所有相邻的节点,然后依次访问下一层的节点。
BFS 实现示例(Java):
import java.util.*;
class GraphBFS {
private final int V;
private final LinkedList<Integer>[] adjList;
GraphBFS(int V) {
this.V = V;
adjList = new LinkedList[V];
for (int i = 0; i < V; i++) {
adjList[i] = new LinkedList<>();
}
}
void addEdge(int src, int dest) {
adjList[src].add(dest);
adjList[dest].add(src); // For undirected graph
}
void BFS(int start) {
boolean[] visited = new boolean[V];
LinkedList<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.add(start);
while (queue.size() != 0) {
int node = queue.poll();
System.out.print(node + " ");
for (Integer neighbor : adjList[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.add(neighbor);
}
}
}
}
}
深度遍历(DFS, Depth-First Search)是一种图遍历算法,优先访问节点的深层次子节点。DFS 可以使用递归或栈来实现。
DFS 实现示例(Java):
import java.util.*;
class GraphDFS {
private final int V;
private final LinkedList<Integer>[] adjList;
GraphDFS(int V) {
this.V = V;
adjList = new LinkedList[V];
for (int i = 0; i < V; i++) {
adjList[i] = new LinkedList<>();
}
}
void addEdge(int src, int dest) {
adjList[src].add(dest);
adjList[dest].add(src); // For undirected graph
}
void DFS(int start) {
boolean[] visited = new boolean[V];
DFSUtil(start, visited);
}
private void DFSUtil(int node, boolean[] visited) {
visited[node] = true;
System.out.print(node + " ");
for (Integer neighbor : adjList[node]) {
if (!visited[neighbor]) {
DFSUtil(neighbor, visited);
}
}
}
}
布隆过滤器是一种用于快速判断元素是否在集合中的概率型数据结构。它具有非常低的误判率(即可能误判一个不存在的元素为存在),但不会误判实际存在的元素为不存在。它使用多个哈希函数和位数组来实现。
import java.util.BitSet;
import java.util.Random;
class BloomFilter {
private BitSet bitSet;
private int bitSize;
private int[] hashSeeds; // 哈希种子
private int numHashFunctions;
public BloomFilter(int bitSize, int numHashFunctions) {
this.bitSize = bitSize;
this.bitSet = new BitSet(bitSize);
this.numHashFunctions = numHashFunctions;
this.hashSeeds = new int[numHashFunctions];
// 生成不同的哈希种子
Random rand = new Random();
for (int i = 0; i < numHashFunctions; i++) {
hashSeeds[i] = rand.nextInt();
}
}
// 简单哈希函数
private int hash(String data, int seed) {
int hash = 0;
for (int i = 0; i < data.length(); i++) {
hash = seed * hash + data.charAt(i);
}
return Math.abs(hash % bitSize);
}
// 添加元素到布隆过滤器
public void add(String data) {
for (int i = 0; i < numHashFunctions; i++) {
int hashValue = hash(data, hashSeeds[i]);
bitSet.set(hashValue);
}
}
// 检查元素是否可能存在
public boolean mightContain(String data) {
for (int i = 0; i < numHashFunctions; i++) {
int hashValue = hash(data, hashSeeds[i]);
if (!bitSet.get(hashValue)) {
return false; // 如果有一位不为1,则元素一定不存在
}
}
return true; // 所有位都为1,则可能存在
}
}
public class Main {
public static void main(String[] args) {
BloomFilter bloomFilter = new BloomFilter(1000, 3);
// 添加元素
bloomFilter.add("hello");
bloomFilter.add("world");
// 查询元素
System.out.println(bloomFilter.mightContain("hello")); // 输出 true
System.out.println(bloomFilter.mightContain("java")); // 输出 false
}
}
布隆过滤器适用于需要快速查询且允许一定误判的场景,例如缓存命中、黑名单检测等。
布隆过滤器是一种空间高效的概率数据结构,用于检测一个元素是否在集合中。它具有以下典型使用场景:
布隆过滤器的核心原理是使用多个哈希函数将元素映射到一个位数组中,并通过这些位数组中的位来判断一个元素是否存在。它的基本实现方式如下:
m
的位数组(通常初始化为全零)。k
个哈希函数,每个哈希函数将元素映射到位数组中的某些位置。k
个哈希函数计算出 k
个位置,并将这些位置的位设置为 1。k
个哈希函数计算出 k
个位置,并检查这些位置的位是否都是 1。如果所有位置的位都是 1,则可能存在;如果有任何位置的位为 0,则一定不存在。布隆过滤器的代码示例(Java):
import java.util.BitSet;
import java.util.function.Function;
public class BloomFilter {
private final BitSet bitSet;
private final int size;
private final Function<String, Integer>[] hashFunctions;
@SuppressWarnings("unchecked")
public BloomFilter(int size, int hashFunctionsCount) {
this.size = size;
bitSet = new BitSet(size);
hashFunctions = new Function[hashFunctionsCount];
for (int i = 0; i < hashFunctionsCount; i++) {
final int seed = i;
hashFunctions[i] = s -> (s.hashCode() + seed) % size;
}
}
public void add(String item) {
for (Function<String, Integer> hashFunction : hashFunctions) {
int hash = hashFunction.apply(item);
bitSet.set(hash);
}
}
public boolean mightContain(String item) {
for (Function<String, Integer> hashFunction : hashFunctions) {
int hash = hashFunction.apply(item);
if (!bitSet.get(hash)) {
return false;
}
}
return true;
}
}
布隆过滤器的准确性主要体现在误报率(false positive rate),它是布隆过滤器返回“存在”时实际并不在集合中的概率。可以通过以下方式提高布隆过滤器的准确性:
m
,可以减少哈希冲突,从而降低误报率。k
,可以减少哈希冲突,但需要在实现时平衡性能和误报率。布隆过滤器的哈希计算方式可以有多种,常见的哈希函数包括:
hash(x) = (a * x + b) % p
,其中 a
和 b
是常数,p
是质数。hash(x) = floor(m * (x * A % 1))
,其中 A
是一个介于 0 和 1 之间的常数。标准布隆过滤器:基本的布隆过滤器实现,如前面所示。
计数布隆过滤器(Counting Bloom Filter):
分层布隆过滤器(Layered Bloom Filter):
弹性布隆过滤器(Cuckoo Bloom Filter):
Guava:
示例代码:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000);
bloomFilter.put(123);
boolean mightContain = bloomFilter.mightContain(123);
RedisBloom:
示例代码:
# 添加元素到布隆过滤器
BF.ADD myBloomFilter "value1"
# 检查元素是否在布隆过滤器中
BF.EXISTS myBloomFilter "value1"
RedisBloom 的具体实现可以参考其 GitHub 仓库:RedisBloom 了解更多细节。
HashMap 是基于什么样的数据结构实现的?
HashMap
是基于哈希表(Hash Table)实现的。它使用数组来存储链表或红黑树(当元素个数较多时)来处理哈希碰撞。HashMap 的哈希桶为什么不用链表实现?
HashMap
在早期版本中是使用链表来实现哈希桶的。当某个桶中的元素数量较多时,链表会变得不够高效。在 Java 8 及之后的版本中,HashMap
对于链表长度超过一定阈值(默认为 8)时,会将链表转换为红黑树,以提高查找效率。HashMap 的哈希计算如何尽可能降低元素的碰撞?
HashMap
通过使用哈希函数(通常是对元素的 hashCode
进行扰动操作)来尽可能均匀地分布元素,降低碰撞的可能性。哈希函数在计算哈希值时会应用一些扰动函数,以减少哈希碰撞的概率。HashMap 用到的什么散列算法;平方散列?斐波那契散列?哈希散列(扰动函数)?
HashMap
的哈希算法主要使用扰动函数来计算哈希值。Java 8 及之后版本使用了一个扰动函数,它通过对 hashCode
进行位运算和混合操作来生成最终的哈希值,这样可以减少碰撞的可能性。具体的扰动函数是 (hash ^ (hash >>> 16))
。HashMap 链表 + 红黑树来装填碰撞元素,AVL 树和二叉堆也是树形结构可以替换红黑树吗,为什么?
HashMap
中的键值存储。HashMap 的红黑树与 2-3 树有什么关系,B-树都包括哪些?Binary SearchTree 是 B-树吗?
HashMap
中以保持操作的时间复杂度。2-3 树是一种 B-树的特例,B-树是一类自平衡的多路查找树。B-树有多个变种,包括 B+树、B*树等。Binary Search Tree
(BST)不是 B-树,而是一种基本的二叉树,其中每个节点的左子树都包含比节点值小的元素,而右子树包含比节点值大的元素。HashMap 的红黑树什么时候左旋、什么时候右旋、什么时候染色?
HashMap
中,具体的旋转和染色操作由红黑树的插入和删除算法来管理。HashMap 的哈希桶,散列元素和布隆过滤器有相似的地方?
HashMap
的哈希桶和布隆过滤器都涉及到哈希技术。HashMap
使用哈希表来存储键值对,并使用哈希函数来减少碰撞;布隆过滤器使用多个哈希函数来检查一个元素是否可能在集合中。两者都依赖于哈希技术,但布隆过滤器更侧重于空间效率和查询是否存在的能力,而 HashMap
更侧重于高效的键值对存储和检索。