散列表的demo實現,二分查找

[size=medium][i][b]散列表,散列算法[/b][/i][/size]
[size=small][b]一、概念
[/b]首先,回顧下[b]散列[/b]的概念。散列同順序、鏈接和索引一樣,是一中數據存儲方法。
定義:以數據集合中的每個元素的關鍵字k爲自變量,通過一個函數h(k)計算出函數值,用這個值作爲一塊連續的存儲空間(數組或文件空間)中的元素存儲位置,將該元素存放在這塊位置上。其中h(k) 成爲散列函數或哈希函數,而h(k)的值成爲散列地址或者哈希地址。
[b]散列表[/b]
根據散列函數計算出的值作爲地址存放相關元素值,這樣的一組數據存放方式稱爲散列表。
[b]衝突[/b]
多個元素的key值計算出的哈希值爲同一個時,就造成了衝突。通常來講,這種具有不同關鍵字而具有相同散列地址的元素稱爲"同義詞",由同義詞引起的存儲衝突稱爲[b]同義詞衝突[/b]。

發生衝突的因素:
1、裝填因子α,其中α=n/m,其中n爲帶插入的元素個數,m爲散列表長度。一般來講,α越小,發生衝突的可能性就越小,但是隨之而來的問題是,空間利用率越低。
2、散列函數的選擇,若散列函數選擇得當,就能使散列地址均勻的分佈在散列空間上,從而減少衝突的發生。
3、解決衝突的方法。
[b]散列桶[/b]
散列表(散列存儲)中每個散列地址對應的存儲位置被稱爲一個[b]桶[/b]。

[b]二、散列函數
[/b]1、直接定址法
h(k) = k + C 其中k爲關鍵字本身,C爲常數。
優點:簡單,沒有衝突發生,如果有衝突,則說明是關鍵字重複。
缺點:若關鍵字不連續,空號較多,將造成存儲空間的較大浪費。
2、除留取餘數法
h(k)=k%m
該方法適用範圍廣,是目前最常見的方法。
3、其他
數字分析法、平方取中法、摺疊法

[b]三、處理衝突的方法
[/b] 主要包含開放定址法(線性探查法、平方探查法、雙散列函數探查法)、鏈接法(即鏈地址法)

散列性能分析,從這本書《數據結構使用教程.徐孝凱》上得到的結果是鏈接地址法的平均查找長度是比較優秀的。

[b]四、散列表的運算
[/b] 進行散列表的運算,首先定義散列表在Java中的接口類,說明該中存儲結構需要有哪些功能,然後給出不同處理衝突方式的實現類。一般常用的有兩種方式:一種是採用開放定址法處理;另一種是鏈接法處理衝突的實現類。

1、根據散列表的存儲性質,定義接口類:

package com.algorithm.hash;

/**
* 散列表的接口類
* Created by xiaoyun on 2016/8/4.
*/
public interface HashTable {
/**
* 向散列表中插入一個關鍵字爲theKey的元素obj,若插入成功返回真否則返回假
* @param theKey
* @param obj
* @return
*/
boolean insert(Object theKey, Object obj);

/**
* 從散列表中查找並返回與給定關鍵字theKey的元素值,若查找失敗則返回假
* @param theKey
* @return
*/
Object search(Object theKey);

/**
* 從散列表中刪除關鍵字爲theKey的元素,若查找失敗返回空
* @param theKey
* @return
*/
boolean delete(Object theKey);

/**
* 返回散列表中的元素個數
* @return
*/
int size();

/**
* 返回散列表的容量,即散列表的空間大小m的值
* @return
*/
int capacity();

/**
* 判斷該散列表是否爲空,如果爲空則返回true,否則返回false
* @return
*/
boolean isEmpty();

/**
* 清空散列表
*/
void clear();

/**
* 輸出散列表中保存的所有關鍵字和對應元素
*/
void output();
}

2、開放定址法處理衝突的數組存儲類

package com.algorithm.hash;

/**
* 採用開放定址法(這裏採用線性探查法)處理衝突的數組存儲類
* Created by xiaoyun on 2016/8/4.
*/
public class SeqHashTable implements HashTable {

/** 保存散列表的容量 */
private int m;

/** 定義保存元素關鍵字的數組 */
private Object[] key;

/** 定義保存散列桶的數組 */
private Object[] ht;

/** 散列表中已有的元素個數 */
private int n;

/** 元素內容被刪除後的關鍵字刪除標記 */
private Object tag;

/**
* 私有方法-散列函數,假定採用除留取餘數法,如果參數不是整數,應設法轉換爲整數
* @param theKey
* @return
*/
private int h(Object theKey) {
return (Integer)theKey % m;
}

/**
* 構造方法
* @param mm 散列表容量
* @param tag 刪除標記
*/
public SeqHashTable(int mm, Object tag) {
if(mm < 13) {
this.m = 13; // 假定散列表的容量至少等於13
}else {
this.m = mm;
}
n = 0;
key = new Object[m];
ht = new Object[m];
this.tag = tag;
}

/**
* 向散列表中插入一個關鍵字爲theKey的新元素obj,插入成功返回true,無可用空間直接退出
* @param theKey
* @param obj
* @return
*/
public boolean insert(Object theKey, Object obj) {
int d = h(theKey);
int temp = d;
while(key[d] != null && !key[d].equals(this.tag)){
if(key[d].equals(theKey)){
break;
}
d = (d + 1)%m; // 探查下一個單元
if(d == temp) {
System.out.println("散列表無該元素的存儲空間,退出運行!");
System.exit(1);
}
}
if(key[d] == null || key[d].equals(this.tag)) { // 找到插入位置,將該元素插入,返回true
key[d] = theKey;
ht[d] = obj;
this.n++;
return true;
}else { // 用新元素obj替換已存在的元素並返回false
ht[d] = obj;
return false;
}
}

/**
* 從散列表中查找並返回與給定關鍵字theKey的元素值,若查找失敗則返回假
* @param theKey
* @return
*/
public Object search(Object theKey) {
int d = h(theKey); // 求theKey的散列地址
int temp = d; // 用temp暫存所求得的散列地址d
while(key[d] != null) { // 當散列地址中的關鍵字不爲空時進行循環
if(key[d].equals(theKey)) {
return ht[d];
}else {
d = (d + 1) % m;
}
if(d == temp) { // 查找一週後失敗返回空值
return null;
}
}
return null;
}

/**
* 從散列表中刪除關鍵字爲theKey的元素,若查找失敗返回空
* @param theKey
* @return
*/
public boolean delete(Object theKey) {
int d = h(theKey);
int temp = d;
while (key[d] != null) { // 散列地址爲空時退出循環
if(key[d].equals(theKey)) { // 找到需要刪除的元素
key[d] = this.tag; // 設置刪除標記
ht[d] = null; // 元素值變爲空
n--; // 散列桶長度減一
return true;
} else {
d = (d + 1) % m; // 線性探測法
}
if(d == temp) {
return false;
}
}
return false;
}

public int size() {
return this.n;
}

public int capacity() {
return this.m;
}

public boolean isEmpty() {
return this.n == 0;
}

public void clear() {
for (int i = 0; i < m; i++) {
key[i] = null;
ht[i] = null;
}
this.n = 0;
}

public void output() {
for (int i = 0; i < m; i++) {
if(key[i] == null || key[i].equals(tag)){
continue;
}
System.out.print("(" + key[i] + " " + ht[i] + "),");
}
System.out.println();
}
}

3、採用鏈接法處理衝突的鏈接存儲類
定義結點類型

package com.algorithm.hash;

/**
* 鏈地址法處理衝突的散列表中的結點類型
* Created by xiaoyun on 2016/8/6.
*/
public class HashNode {
Object key;
Object element;
HashNode next;
public HashNode(Object key, Object element) {
this.key = key;
this.element = element;
next = null;
}
}

定義存儲結構:

package com.algorithm.hash;

/**
* 鏈接法處理衝突的散列表
* Created by xiaoyun on 2016/8/6.
*/
public class LinkHashTable implements HashTable {

private int m; // 保存散列表的容量

private HashNode[] ht; // 定義保存散列表的數組

private int n; // 散列表中已有元素的個數

/**
* 定義散列函數,除留取餘數法
* @param key
* @return
*/
private int h(Object key) {
return (Integer)key%this.m;
}

public LinkHashTable(int m) {
if(m < 13) {
this.m = 13;
}else {
this.m = m;
}
n = 0;
ht = new HashNode[this.m];
}

/**
* 向散列表中插入一個關鍵字爲theKey的元素obj,若插入成功返回真否則返回假
* @param theKey
* @param obj
* @return
*/
public boolean insert(Object theKey, Object obj) {
int d = h(theKey);
HashNode p = ht[d];
while (p != null) {
if(p.key.equals(theKey)){
break;
}else {
p = p.next;
}
}
if(p != null) { // 用新值替換舊值,返回false
p.element = obj;
return false;
}else { // 將新結點插入到對應單鏈表的表頭並返回真
p = new HashNode(theKey, obj);
p.next = ht[d];
ht[d] = p;
n++;
return true;
}
}

/**
* 從散列表中查找並返回與給定關鍵字theKey的元素值,若查找失敗則返回假
* @param theKey
* @return
*/
public Object search(Object theKey) {
int d = h(theKey);
HashNode p = ht[d];
while (p != null) {
if(p.key.equals(theKey)){
return p.element;
}
p = p.next;
}
return null;
}

public boolean delete(Object theKey) {
int d = h(theKey);
HashNode p = ht[d]; // p指向表頭結點
HashNode q = null; // q指向前驅結點
while (p != null) { // 循環查找被刪除的結點
if(p.key.equals(theKey)){
break;
}else {
q = p;
p = p.next;
}
}
if(p == null){
return false;
}
if(q == null) {
ht[d] = p.next;
}else {
q.next = p.next;
}
n--;
return true;
}

public int size() {
return this.n;
}

public int capacity() {
return this.m;
}

public boolean isEmpty() {
return this.n == 0;
}

public void clear() {
for (int i = 0; i < m; i++) {
ht[i] = null;
}
n = 0;
}

public void output() {
for (int i = 0; i < m; i++) {
HashNode p = ht[i];
while(p != null) {
System.out.print("(" + p.key +":" + p.element+"),");
p = p.next;
}
}
System.out.println();
}
}

4、散列表運算的調試程序

import com.algorithm.hash.HashTable;
import com.algorithm.hash.LinkHashTable;
import com.algorithm.hash.SeqHashTable;

/**
* 散列表算法測試類
* Created by admin on 2016/8/6.
*/
public class Example10_3 {
public static void main(String[] args) {
// 關鍵字數組
int[] a = {18,75,60,43,54,90,46,31,58,73,15,34};
// 要存放的元素數組
String[] b = {"180","750","600","430", "540", "900","460","310","580","730","150","340"};
// 初始化開放地址法處理衝突的散列表
//HashTable tb = new SeqHashTable(17, -1);
// 鏈地址法處理衝突的散列表
HashTable tb = new LinkHashTable(17);
for (int i = 0; i < a.length; i++) {
tb.insert(a[i], b[i]);
}
System.out.println("輸出散列表中的所有元素:");
tb.output();
System.out.println("散列表容量:" + tb.capacity());
System.out.println("散列表中的元素個數:" + tb.size());
// 刪除元素
for (int i = 0; i < a.length; i += 3) {
tb.delete(a[i]);
}
// 修改元素
tb.insert(75, "880");
System.out.println("經插入、刪除、修改後的散列表爲:");
tb.output();
System.out.println("散列表容量:" + tb.capacity());
System.out.println("散列表中的元素個數:" + tb.size());
System.out.println("查找元素的結果:");
for (int i = 0; i < 4; i++) {
Object x = tb.search(a[i]);
if(x == null) {
System.out.println("key爲 " + a[i] + "關鍵字的元素未找到!");
}else {
System.out.println("key爲 " + a[i] + "對應的元素值爲:" + x);
}
}
}
}

運行結果:
輸出散列表中的所有元素:
(34:340),(18:180),(54:540),(73:730),(90:900),(58:580),(75:750),(43:430),(60:600),(46:460),(31:310),(15:150),
散列表容量:17
散列表中的元素個數:12
經插入、刪除、修改後的散列表爲:
(34:340),(54:540),(90:900),(58:580),(75:880),(60:600),(31:310),(15:150),
散列表容量:17
散列表中的元素個數:8
查找元素的結果:
key爲 18關鍵字的元素未找到!
key爲 75對應的元素值爲:880
key爲 60對應的元素值爲:600
key爲 43關鍵字的元素未找到!

以上爲使用鏈接法處理衝突的算法調試結果。
將Example10_3.java的18行註釋掉,去掉16行的註釋,則爲測試開放定址法的數組存儲類,執行結果爲:
輸出散列表中的所有元素:
(34 340),(18 180),(54 540),(90 900),(73 730),(75 750),(58 580),(60 600),(43 430),(46 460),(31 310),(15 150),
散列表容量:17
散列表中的元素個數:12
經插入、刪除、修改後的散列表爲:
(34 340),(54 540),(90 900),(75 880),(58 580),(60 600),(31 310),(15 150),
散列表容量:17
散列表中的元素個數:8
查找元素的結果:
key爲 18關鍵字的元素未找到!
key爲 75對應的元素值爲:880
key爲 60對應的元素值爲:600
key爲 43關鍵字的元素未找到!
[/size]

[size=medium][i][b]二分查找[/b][/i][/size]
定義:二分查找又稱爲折半查找,是一種能對有序表進行快速查找的方法,時間複雜度爲O(log₂N)。

package com.data.search.binary;

/**
* 二分查找算法
* 二分查找定義:二分查找又稱爲折半查找,首先,二分查找是針對有序表a(這裏認爲是升序)進行查找,首先取數組中的中間值a[mid],同給定的值x進行比較,如果x小於中間值,那麼max=mid-1,min=0,mid=(max-min)/2,
* 繼續比較a[mid]與x的大小,然後依次遞歸,直到找到對應的元素或者查找區間爲空爲止。
* Created by xiaoyun on 2016/8/1.
*/
public class BinarySearch {
/**
* 從數組的前n個元素中二分查找值爲x的元素
* @param a
* @param x
* @param n
* @return
* 時間複雜度:二分查找過程可用一棵二叉樹來描述,它的左子樹和右子樹分別代碼對應區間的左子表和右子表,通常把這樣的二叉樹稱爲判定樹,
* 進行二分查找的判定樹不僅是一棵二叉搜索樹,而且是一顆理想二叉樹,因爲除了最後一層外,其餘所有層的結點數都是滿的。
* 所以二分查找的時間複雜度爲:O(log₂N),其中N爲元素個數。
* 優點:比較次數少,查找速度快。
* 缺點:查找前需要建立有序表,這需要付出一定代價,同時對該有序表進行插入和刪除操作都需要平均比較和移動表中的一半元素,是浪費時間的操作。
* 應用場景:適用於順序存儲、並且不經常進行插入和刪除的有序表。不適用於鏈接存儲的有序表。
*/
public static int binarySearch(Object[] a, Object x, int n) {
int low = 0;
int high = n - 1;
while(low <= high) {
int mid = (low + high)/2;
// 比較該下標代表的值與目標值
int result = ((Comparable)a[mid]).compareTo(x);
if(result == 0) {
return mid;
}else if(result > 0) { // 查找左區間
high = mid - 1;
}else { // 查找右區間
low = mid + 1;
}
}
return -1; // 查找失敗返回-1
}

public static void main(String[] args) {
Object[] a = {1,2,3,4,5,6,7,8,9,10,11};
System.out.println(binarySearch(a, 5, 5));
}
}

優點:比較次數少,查找速度快。
缺點:查找前需要建立有序表,而頻繁的插入和刪除平均每次需要移動表中的一半元素,浪費時間。
適用場景:數據穩定,很少進行插入或刪除運算的情況。也就是說,二分查找只適用於順序存儲的有序表,不適於連接存儲的有序表。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章