折半查詢與二叉查詢樹
在生活當中,我們可能每天都要進行查詢工作,字典中查詢,搜尋引擎中查詢,資料庫中進行查詢。在這個資訊的時代下,我們每天都要從網際網路上接觸到很多資訊。這些資訊從哪裡來,當然是儲存在資料庫中。提到資料庫,大家首先想到的肯定是索引,是的資料庫的優劣很大是與索引相關。而索引是為了什麼,就是為了方便查詢,快速從資料庫中提取我們想要的資料,對於索引的優化也是一個難點。下面筆者就來介紹一下折半查詢和建立二叉查詢樹進行查詢。
折半查詢:
大家知道,順序查詢(線性查詢)是從線性表中的一端到另一端逐個進行比較,當資料非常大時,查詢效率非常低,時間複雜度為O(n),但是它對記錄的儲存沒有任何要求(記錄不必有序)。
折半查詢
基本思想:(1)在有序資料中,去中間的記錄作為比較物件,若給定的值與中間記錄相等,則查詢成功。
(2) 若給定的值小於中間記錄,則在中間記錄的左半區進行查詢。
(3) 若給定的值大於中間記錄,則在中間記錄的右半區進行查詢。
(4) 重複以上步驟,直到查詢成功,若查詢的區域無記錄,則查詢失敗。
下面筆者定義了一個折半查詢的類: 使用數字進行儲存資料。
插入資料: 由於折半查詢要求資料必須有序,並且插入操作往往比查詢操作頻率更加小。所以在插入時就對資料進行排序。private Object[] array; private int size; // 陣列的大小 private BinarySearch(int max) { //通過建構函式初始化陣列的大小 array = new Object[max]; size = 0; }
/** 插入元素 */ @SuppressWarnings("unchecked") public void insert(T value) { int pos = 0; while(pos < size){ if (((Comparable<? super T>) array[pos]).compareTo(value) > 0) break; pos++; } for (int k = size; k > pos; k--) { //將比插入值大的元素後移 array[k] = array[k - 1]; } array[pos] = value; size++; }
折半查詢: 這裡使用的是遞迴進行查詢,時間複雜度為O(logN)。
public int find(T searchKey) {
return reFind(searchKey, 0, size - 1);
}
/**
* 通過將關鍵字與查詢部分的中間元素比較,並呼叫自身實現遞迴呼叫,直到找到關鍵字
*
* @param searchKey 需要尋找的關鍵字
* @param lower 查詢部分的開始
* @param upper 查詢部分的結尾
* @return 成功找到返回關鍵字位置,失敗則返回 -1
*/
@SuppressWarnings("unchecked")
public int reFind(T searchKey, int lower, int upper) {
int current;
current = (lower + upper) / 2;
if (array[current] == searchKey) { // 找到關鍵字直接返回關鍵字位置
return current;
} else if (lower > upper) { // lower、upper交錯,不能找到,直接返回陣列大小
return -1;
} else {
if (((Comparable<? super T>) array[current]).compareTo(searchKey) < 0) { // 關鍵字在小於 array[current] 的一半查詢
return reFind(searchKey, current + 1, upper);
} else {
return reFind(searchKey, lower, current - 1); // 關鍵字在大於array[current]的一半查詢
}
}
}
測試:
package org.TT.Recursion;
import java.util.Scanner;
/** 使用遞迴進行二分查詢。
* 遞迴的二分查詢程式碼簡潔,時間複雜度為O(logN)
*/
public class BinarySearch<T extends Comparable<? super T>> {
// 這裡的陣列需要已經排好序,在插入時就直接排序
private Object[] array;
private int size; // 陣列的大小
private BinarySearch(int max) { //通過建構函式初始化陣列的大小
array = new Object[max];
size = 0;
}
/** 取得查詢陣列的大小
*/
public int size() {
return size;
}
public int find(T searchKey) {
return reFind(searchKey, 0, size - 1);
}
/**
* 通過將關鍵字與查詢部分的中間元素比較,並呼叫自身實現遞迴呼叫,直到找到關鍵字
*
* @param searchKey 需要尋找的關鍵字
* @param lower 查詢部分的開始
* @param upper 查詢部分的結尾
* @return 成功找到返回關鍵字位置,失敗則返回 -1
*/
@SuppressWarnings("unchecked")
public int reFind(T searchKey, int lower, int upper) {
int current;
current = (lower + upper) / 2;
if (array[current] == searchKey) { // 找到關鍵字直接返回關鍵字位置
return current;
} else if (lower > upper) { // lower、upper交錯,不能找到,直接返回陣列大小
return -1;
} else {
if (((Comparable<? super T>) array[current]).compareTo(searchKey) < 0) { // 關鍵字在小於 array[current] 的一半查詢
return reFind(searchKey, current + 1, upper);
} else {
return reFind(searchKey, lower, current - 1); // 關鍵字在大於array[current]的一半查詢
}
}
}
/** 插入元素
*/
@SuppressWarnings("unchecked")
public void insert(T value) {
int pos = 0;
while(pos < size){
if (((Comparable<? super T>) array[pos]).compareTo(value) > 0)
break;
pos++;
}
for (int k = size; k > pos; k--) { //將比插入值大的元素後移
array[k] = array[k - 1];
}
array[pos] = value;
size++;
}
/**
* 展示需要查詢的所有陣列值
*/
public void dispaly() {
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
}
@SuppressWarnings("resource")
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
BinarySearch<Integer> search = new BinarySearch<>(10);
search.insert(10);
search.insert(90);
search.insert(80);
search.insert(50);
search.insert(30);
search.insert(20);
search.insert(40);
search.insert(700);
System.out.print("陣列元素為: ");
search.dispaly();
System.out.println();
System.out.print("請輸入需要查詢的元素:");
int searchNum = in.nextInt();
if (search.find(searchNum) != -1) {
System.out.println("Found: " + searchNum);
} else {
System.out.println("Can't find " + searchNum);
}
}
}
建立二叉查詢樹進行查詢
二叉查詢樹:
性質: 若它的左子樹不為空,則左子樹上所有節點的值均小於根節點; 若它的右子樹不為空,則右子樹上所有節點的值均小於根節點; 它的左右子樹都是二叉查詢樹。
首先構造一個結點類,儲存資料,並且有左孩子,右孩子的引用。在這裡屬性不設定成私有主要是為了便於訪問,若想設為私有屬性,可以設定get 、set方法訪問。
package org.TT.BinarySeachTree;
/**結點資料結構*/
public class BinaryNode<T>{
T data;
BinaryNode<T> left;
BinaryNode<T> right;
public BinaryNode(T data) {
this(data,null,null);
}
public BinaryNode( T data, BinaryNode<T> left, BinaryNode<T> right) {
this.data =data;
this.left = left;
this.right =right;
}
}
查詢樹: 只已知一個根節點,建構函式初始化一棵空樹。
判斷是否為空:直接判斷根節點是否為空。
清除樹,直接將根節點的應用置為空即可,虛擬機器可進行垃圾回收是發現引用為空,將進行垃圾回收。
<span style="white-space:pre"> </span>private BinaryNode<T> rootTree;
<span style="white-space:pre"> </span>/** 構造一顆空的二叉查詢樹 */
<span style="white-space:pre"> </span>public BinarySearchTree() {
<span style="white-space:pre"> </span>rootTree = null;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>/** 清空二叉查詢樹 */
<span style="white-space:pre"> </span>public void clear() {
<span style="white-space:pre"> </span>rootTree = null;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>/** 判斷是否為空 */
<span style="white-space:pre"> </span>public boolean isEmpty() {
<span style="white-space:pre"> </span>return rootTree == null;
<span style="white-space:pre"> </span>}
插入: 每次插入尋找正確的位置並插入,保證插入後還是一棵二叉查詢樹。
/** 插入元素 */
public void insert(T t) {
rootTree = insert(t, rootTree); // 第一次插入時,直接建立新節點並賦值給根節點
}
/** 在某個位置開始判斷插入元素,採用遞迴實現不斷尋找插入位置,程式碼簡潔*/
public BinaryNode<T> insert(T t, BinaryNode<T> parent) {
if (parent == null) {
return new BinaryNode<T>(t, null, null); // 建立一個新節點
}
int result = t.compareTo(parent.data); //若 t 小於 parent 返回負數, 大於返回正數,等於返回0
if (result < 0)
parent.left = insert(t, parent.left);
else if (result > 0)
parent.right = insert(t, parent.right);
else
;// 若兩個元素相等,什麼也做,如果有需要可以在節點記錄裡附加域來指示重複元素插入的資訊
return parent;
}
插入過程:
查詢: 每次查詢與根結點比較,(1)相等則返回 (2)小於,在左子樹中查詢 (3)大於,在右子樹中查詢
/** 查詢指定的元素,預設從根結點出開始查詢 */
public boolean find(T t) {
return find(t, rootTree);
}
/** 從某個結點出開始查詢元素,
* 用遞迴實現並不斷將關鍵字與父節點比較,大於往右子樹走,小於往左子樹走等於返回 */
public boolean find(T t, BinaryNode<T> node) {
if (node == null)
return false;
int result = t.compareTo(node.data);
if (result > 0)
return find(t, node.right);
else if (result < 0)
return find(t, node.left);
else
return true;
}
查詢最大值、最小值: 若查詢最小值,直接在左子樹上查詢,一直往左查詢。 若查詢最大值,直接在右子樹上查詢,一直往右查詢。 /** 查詢二叉查詢樹中的最小值 */
public T findMin() {
if (isEmpty()) {
System.out.println("二叉樹為空");
return null;
} else
return findMin(rootTree).data;
}
/** 查找出最小元素所在的結點 */
public BinaryNode<T> findMin(BinaryNode<T> node) {
if (node == null)
return null;
else if (node.left == null)
return node;
return findMin(node.left);// 尾遞迴查詢
}
/** 查詢二叉查詢樹中的最大值 */
public T findMax() {
if (isEmpty()) {
System.out.println("二叉樹為空");
return null;
} else
return findMax(rootTree).data;
}
/** 查找出最大元素所在的結點 */
public BinaryNode<T> findMax(BinaryNode<T> node) {
if (node != null) {
while (node.right != null)
node = node.right;
}
return node;
}
刪除: 刪除結點才用遞迴實現。
(1) 刪除結點是葉子節點,直接刪除 。
(2)只有左子樹、右子樹,只需重新連線。
(3)刪除有兩個孩子的節點:用右子樹最小節點代替刪除節點,並刪除右子樹最小節點
/** 刪除元素 */
public void remove(T t) {
rootTree = remove(t, rootTree);
}
/** 在某個位置開始判斷刪除某個結點,同樣用遞迴實現 */
public BinaryNode<T> remove(T t, BinaryNode<T> node) {
if (node == null)
return node;
int result = t.compareTo(node.data);
if (result > 0)
node.right = remove(t, node.right);
else if (result < 0)
node.left = remove(t, node.left);
else if (node.left != null && node.right != null) {
// 刪除有兩個孩子的節點:用右子樹最小節點代替刪除節點,並刪除右子樹最小節點
node.data = findMin(node.right).data; // 找到刪除節點右子樹的最小節點
node.right = remove(node.data, node.right); // 刪除右子樹最小節點
} else
node = (node.left != null) ? node.left : node.right;
//刪除沒有子節點的節點(葉子)和只有一個節點的節點
return node;
}
遍歷及測試:
package org.TT.BinarySeachTree;
/**
* 二叉查詢樹的性質:
* 1.對於樹中的每個節點X,它的左子樹中所有項的值小於X,而它的右子樹中所有項的值大於X,
* Java實現二叉查詢樹(遞迴的編寫二叉查詢樹的各種操作)
* 2.樹的所有節點的平均深度為 O(logN)
* 3.所有操作的平均執行時間也是 O(logN)
* 4.此處的操作:插入結點,構造二叉查詢樹、清空二叉樹、判斷樹是否為空、查詢指定結點、刪除結點、查詢最大值、查詢最小值
*/
public class BinarySearchTree<T extends Comparable<? super T>> {
private BinaryNode<T> rootTree;
/** 構造一顆空的二叉查詢樹 */
public BinarySearchTree() {
rootTree = null;
}
/** 清空二叉查詢樹 */
public void clear() {
rootTree = null;
}
/** 判斷是否為空 */
public boolean isEmpty() {
return rootTree == null;
}
/** 插入元素 */
public void insert(T t) {
rootTree = insert(t, rootTree); // 第一次插入時,直接建立新節點並賦值給根節點
}
/** 在某個位置開始判斷插入元素,採用遞迴實現不斷尋找插入位置,程式碼簡潔*/
public BinaryNode<T> insert(T t, BinaryNode<T> parent) {
if (parent == null) {
return new BinaryNode<T>(t, null, null); // 建立一個新節點
}
int result = t.compareTo(parent.data); //若 t 小於 parent 返回負數, 大於返回正數,等於返回0
if (result < 0)
parent.left = insert(t, parent.left);
else if (result > 0)
parent.right = insert(t, parent.right);
else
;// 若兩個元素相等,什麼也做,如果有需要可以在節點記錄裡附加域來指示重複元素插入的資訊
return parent;
}
/** 查詢指定的元素,預設從根結點出開始查詢 */
public boolean find(T t) {
return find(t, rootTree);
}
/** 從某個結點出開始查詢元素,
* 用遞迴實現並不斷將關鍵字與父節點比較,大於往右子樹走,小於往左子樹走等於返回 */
public boolean find(T t, BinaryNode<T> node) {
if (node == null)
return false;
int result = t.compareTo(node.data);
if (result > 0)
return find(t, node.right);
else if (result < 0)
return find(t, node.left);
else
return true;
}
/** 查詢二叉查詢樹中的最小值 */
public T findMin() {
if (isEmpty()) {
System.out.println("二叉樹為空");
return null;
} else
return findMin(rootTree).data;
}
/** 查找出最小元素所在的結點 */
public BinaryNode<T> findMin(BinaryNode<T> node) {
if (node == null)
return null;
else if (node.left == null)
return node;
return findMin(node.left);// 尾遞迴查詢
}
/** 查詢二叉查詢樹中的最大值 */
public T findMax() {
if (isEmpty()) {
System.out.println("二叉樹為空");
return null;
} else
return findMax(rootTree).data;
}
/** 查找出最大元素所在的結點 */
public BinaryNode<T> findMax(BinaryNode<T> node) {
if (node != null) {
while (node.right != null)
node = node.right;
}
return node;
}
/** 刪除元素 */
public void remove(T t) {
rootTree = remove(t, rootTree);
}
/** 在某個位置開始判斷刪除某個結點,同樣用遞迴實現 */
public BinaryNode<T> remove(T t, BinaryNode<T> node) {
if (node == null)
return node;
int result = t.compareTo(node.data);
if (result > 0)
node.right = remove(t, node.right);
else if (result < 0)
node.left = remove(t, node.left);
else if (node.left != null && node.right != null) {
// 刪除有兩個孩子的節點:用右子樹最小節點代替刪除節點,並刪除右子樹最小節點
node.data = findMin(node.right).data; // 找到刪除節點右子樹的最小節點
node.right = remove(node.data, node.right); // 刪除右子樹最小節點
} else
node = (node.left != null) ? node.left : node.right;
//刪除沒有子節點的節點(葉子)和只有一個節點的節點
return node;
}
/** 前序遍歷 */
public void preOrder(BinaryNode<T> node) {
if (node != null) {
System.out.print(node.data + ",");
preOrder(node.left);
preOrder(node.right);
}
}
/** 後序遍歷 */
public void postOrder(BinaryNode<T> node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.print(node.data + ",");
}
}
/** 中序遍歷 */
public void inOrder(BinaryNode<T> node) {
if (node != null) {
inOrder(node.left);
System.out.print(node.data + ",");
inOrder(node.right);
}
}
public static void main(String[] args) {
int[] value = { 6,8,3,7,10,1,9 };
BinarySearchTree<Integer> tree = new BinarySearchTree<>();
for (int v : value) {
tree.insert(v);
}
System.out.println("前序遍歷");
tree.preOrder(tree.rootTree);
System.out.println();
System.out.println("後序遍歷");
tree.postOrder(tree.rootTree);
System.out.println();
System.out.println("中序遍歷");
tree.inOrder(tree.rootTree);
System.out.println();
System.out.println("最小值==" + tree.findMin());
System.out.println("最大值==" + tree.findMax());
System.out.println("查詢8: " + tree.find(8));
System.out.println("是否為空: " + tree.isEmpty());
System.out.println();
tree.remove(8);
System.out.println("刪除節點值為8的節點,再中序遍歷");
System.out.println("查詢8: " + tree.find(8));
tree.inOrder(tree.rootTree);
}
}