Datawhale 系列數據結構
Task4.1 散列表
基本概念
散列表(Hash Table,又叫哈希表),是根據關鍵碼值(Key Value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
散列表思想
(1)使用散列函數將給定鍵轉化爲一個“數組的索引”,理想情況下,不同的key會被轉化爲不同的索引,但是在實際情況中,我們會遇到不同的鍵轉化爲相同索引的情況,這種情況叫做散列衝突/碰撞,後文中會詳細講解;
(2)得到了索引後,我們就可以像訪問數組一樣,通過這個索引訪問到相應的鍵值對。
如何設計散列函數
(1)散列函數的設計不能太複雜:減少計算時間
(2)散列函數整成的值要儘可能隨機並且均勻分佈
主要方法有:
a.直接尋址法
b.數字分析法
c.平方取中法
d.摺疊法
e.隨機數法
f.除留取餘法
散列衝突
再好的散列函數也無法避免散列衝突
主要方法有:
直接尋址法
鏈表法:更常用,4.1.1基於其設計散列表
4.1.1實現一個基於鏈表解決衝突問題的散列表
/*布穀鳥散列概述
使用hashA、hashB計算對應的key位置:
1、兩個位置均爲空,則任選一個插入;
2、兩個位置中一個爲空,則插入到空的那個位置
3、兩個位置均不爲空,則踢出一個位置後插入,被踢出的對調用該算法,再執行該算法找其另一個位置,循環直到插入成功。
4、如果被踢出的次數達到一定的閾值,則認爲hash表已滿,並進行重新哈希rehash
cuckoo hashing的哈希函數是成對的(具體的實現可以根據需求設計),每一個元素都是兩個,分別映射到兩個位置,一個是記錄的位置,另一個是備用位置。這個備用位置是處理碰撞時用的,cuckoo hashing處理碰撞的方法,就是把原來佔用位置的這個元素踢走,不過被踢出去的元素還有一個備用位置可以安置,如果備用位置上還有人,再把它踢走,如此往復。直到被踢的次數達到一個上限,才確認哈希表已滿,並執行rehash操作
*/
interface HashFamily<AnyType>{
//根據which來選擇散列函數,並返回hash值
int hash(AnyType x,int which);
//返回集合中散列的個數
int getNumberOfFunctions();
//獲取新的散列函數
void generateNewFunctions();
}
class CuckooHashTable<AnyType>{
//定義最大裝填因子爲0.4
private static final double MAX_LOAD = 0.4;
//定義rehash次數達到一定時,進行再散列
private static final int ALLOWED_REHASHES = 1;
//定義默認表的大小
private static final int DEFAULT_TABLE_SIZE = 101;
//定義散列函數集合
private final HashFamily<? super AnyType> hashFunctions;
//定義散列函數個數
private final int numHashFunctions;
//定義當前表
private AnyType[] array;
//定義當前表的大小
private int currentSize;
//定義rehash的次數
private int rehashes = 0;
//定義一個隨機數
private Random r = new Random();
public CuckooHashTable(HashFamily<? super AnyType> hf){
this(hf, DEFAULT_TABLE_SIZE);
}
public void printArray() {
// TODO Auto-generated method stub
}
//初始化操作
public CuckooHashTable(HashFamily<? super AnyType> hf, int size){
allocateArray(nextPrime(size));
doClear();
hashFunctions = hf;
numHashFunctions = hf.getNumberOfFunctions();
}
private int nextPrime(int size) {
return size*2;
}
public void makeEmpty(){
doClear();
}
//清空操作
private void doClear(){
currentSize = 0;
for (int i = 0; i < array.length; i ++){
array[i] = null;
}
}
//初始化表
@SuppressWarnings("unchecked")
private void allocateArray(int arraySize){
array = (AnyType[]) new Object[arraySize];
}
/**
*
* @param x 當前的元素
* @param which 選取的散列函數對應的位置
* @return
*/
private int myHash(AnyType x, int which){
//調用散列函數集合中的hash方法獲取到hash值
int hashVal = hashFunctions.hash(x, which);
//再做一定的處理
hashVal %= array.length;
if (hashVal < 0){
hashVal += array.length;
}
return hashVal;
}
/**
* 查詢元素的位置,若找到元素,則返回其當前位置,否則返回-1
* @param x
* @return
*/
private int findPos(AnyType x){
//遍歷散列函數集合,因爲不確定元素所用的散列函數爲哪個
for (int i = 0; i < numHashFunctions; i ++){
//獲取到當前hash值
int pos = myHash(x, i);
//判斷表中是否存在當前元素
if (array[pos] != null && array[pos].equals(x)){
return pos;
}
}
return -1;
}
public boolean contains(AnyType x){
return findPos(x) != -1;
}
/**
* 刪除元素:先查詢表中是否存在該元素,若存在,則進行刪除該元素
* @param x
* @return
*/
public boolean remove(AnyType x){
int pos = findPos(x);
if (pos != -1){
array[pos] = null;
currentSize --;
}
return pos != -1;
}
/**
* 插入:先判斷該元素是否存在,若存在,在判斷表的大小是否達到最大負載,
* 若達到,則進行擴展,最後調用insertHelper方法進行插入元素
* @param x
* @return
*/
public boolean insert(AnyType x){
if (contains(x)){
return false;
}
if (currentSize >= array.length * MAX_LOAD){
expand();
}
return insertHelper(x);
}
private boolean insertHelper(AnyType x) {
//記錄循環的最大次數
final int COUNT_LIMIT = 100;
while (true){
//記錄上一個元素位置
int lastPos = -1;
int pos;
//進行查找插入
for (int count = 0; count < COUNT_LIMIT; count ++){
for (int i = 0; i < numHashFunctions; i ++){
pos = myHash(x, i);
//查找成功,直接返回
if (array[pos] == null){
array[pos] = x;
currentSize ++;
return true;
}
}
//查找失敗,進行替換操作,產生隨機數位置,當產生的位置不能與原來的位置相同
int i = 0;
do {
pos = myHash(x, r.nextInt(numHashFunctions));
} while (pos == lastPos && i ++ < 5);
//進行替換操作
AnyType temp = array[lastPos = pos];
array[pos] = x;
x = temp;
}
//超過次數,還是插入失敗,則進行擴表或rehash操作
if (++ rehashes > ALLOWED_REHASHES){
expand();
rehashes = 0;
} else {
rehash();
}
}
}
private void expand(){
rehash((int) (array.length / MAX_LOAD));
}
private void rehash(){
hashFunctions.generateNewFunctions();
rehash(array.length);
}
private void rehash(int newLength){
AnyType [] oldArray = array;
allocateArray(nextPrime(newLength));
currentSize = 0;
for (AnyType str : oldArray){
if (str != null){
insert(str);
}
}
}
}
4.1.2 實現一個LRU緩存淘汰算法
class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
/**
*
*/
private static final long serialVersionUID = 1L;
private final int maxCapacity;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private final Lock lock = new ReentrantLock();
public LRULinkedHashMap(int maxCapacity) {
super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return size() > maxCapacity;
}
@Override
public boolean containsKey(Object key) {
try {
lock.lock();
return super.containsKey(key);
} finally {
lock.unlock();
}
}
@Override
public V get(Object key) {
try {
lock.lock();
return super.get(key);
} finally {
lock.unlock();
}
}
@Override
public V put(K key, V value) {
try {
lock.lock();
return super.put(key, value);
} finally {
lock.unlock();
}
}
public int size() {
try {
lock.lock();
return super.size();
} finally {
lock.unlock();
}
}
public void clear() {
try {
lock.lock();
super.clear();
} finally {
lock.unlock();
}
}
public Collection<Map.Entry<K, V>> getAll() {
try {
lock.lock();
return new ArrayList<Map.Entry<K, V>>(super.entrySet());
} finally {
lock.unlock();
}
}
}
4.1.3 練習:兩數之和
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}
TASK4.2 字符串
4.2.1 實現一個字符集,只包含這26個英文字母的Trie樹
class Trie_Tree{
private class Node{
private int dumpli_num;////該字串的反覆數目, 該屬性統計反覆次數的時候實用,取值爲0、1、2、3、4、5……
private int prefix_num;///以該字串爲前綴的字串數。 應該包含該字串本身。。!
private Node childs[];////此處用數組實現,當然也能夠map或list實現以節省空間
private boolean isLeaf;///是否爲單詞節點
public Node(){
dumpli_num=0;
prefix_num=0;
isLeaf=false;
childs=new Node[26];
}
}
private Node root;///樹根
public Trie_Tree(){
///初始化trie 樹
root=new Node();
}
/**
* 插入字串。用循環取代迭代實現
* @param words
*/
public void insert(String words){
insert(this.root, words);
}
/**
* 插入字串,用循環取代迭代實現
* @param root
* @param words
*/
private void insert(Node root,String words){
words=words.toLowerCase();////轉化爲小寫
char[] chrs=words.toCharArray();
for(int i=0,length=chrs.length; i<length; i++){
///用相對於a字母的值作爲下標索引,也隱式地記錄了該字母的值
int index=chrs[i]-'a';
if(root.childs[index]!=null){
////已經存在了,該子節點prefix_num++
root.childs[index].prefix_num++;
}else{
///假設不存在
root.childs[index]=new Node();
root.childs[index].prefix_num++;
}
///假設到了字串結尾,則做標記
if(i==length-1){
root.childs[index].isLeaf=true;
root.childs[index].dumpli_num++;
}
///root指向子節點,繼續處理
root=root.childs[index];
}
}
/**
* 遍歷Trie樹,查找全部的words以及出現次數
* @return HashMap<String, Integer> map
*/
public HashMap<String,Integer> getAllWords(){
// HashMap<String, Integer> map=new HashMap<String, Integer>();
return preTraversal(this.root, "");
}
/**
* 前序遍歷。。。
* @param root 子樹根節點
* @param prefixs 查詢到該節點前所遍歷過的前綴
* @return
*/
private HashMap<String,Integer> preTraversal(Node root,String prefixs){
HashMap<String, Integer> map=new HashMap<String, Integer>();
if(root!=null){
if(root.isLeaf==true){
////當前即爲一個單詞
map.put(prefixs, root.dumpli_num);
}
for(int i=0,length=root.childs.length; i<length;i++){
if(root.childs[i]!=null){
char ch=(char) (i+'a');
////遞歸調用前序遍歷
String tempStr=prefixs+ch;
map.putAll(preTraversal(root.childs[i], tempStr));
}
}
}
return map;
}
/**
* 推斷某字串是否在字典樹中
* @param word
* @return true if exists ,otherwise false
*/
public boolean isExist(String word){
return search(this.root, word);
}
/**
* 查詢某字串是否在字典樹中
* @param word
* @return true if exists ,otherwise false
*/
private boolean search(Node root,String word){
char[] chs=word.toLowerCase().toCharArray();
for(int i=0,length=chs.length; i<length;i++){
int index=chs[i]-'a';
if(root.childs[index]==null){
///假設不存在,則查找失敗
return false;
}
root=root.childs[index];
}
return true;
}
/**
* 得到以某字串爲前綴的字串集。包含字串本身。 相似單詞輸入法的聯想功能
* @param prefix 字串前綴
* @return 字串集以及出現次數,假設不存在則返回null
*/
public HashMap<String, Integer> getWordsForPrefix(String prefix){
return getWordsForPrefix(this.root, prefix);
}
/**
* 得到以某字串爲前綴的字串集。包含字串本身。
* @param root
* @param prefix
* @return 字串集以及出現次數
*/
private HashMap<String, Integer> getWordsForPrefix(Node root,String prefix){
HashMap<String, Integer> map=new HashMap<String, Integer>();
char[] chrs=prefix.toLowerCase().toCharArray();
////
for(int i=0, length=chrs.length; i<length; i++){
int index=chrs[i]-'a';
if(root.childs[index]==null){
return null;
}
root=root.childs[index];
}
///結果包含該前綴本身
///此處利用之前的前序搜索方法進行搜索
return preTraversal(root, prefix);
}
}
4.2.2 實現樸素的字符串匹配算法
public static int indext(String src, String target) {
return indext(src,target,0);
}
public static int indext(String src, String target, int fromIndex) {
return indext(src.toCharArray(), src.length(), target.toCharArray(), target.length(), fromIndex);
}
//樸素模式匹配算法
static int indext(char[] s, int slen, char[] t, int tlen, int fromIndex) {
if (fromIndex < 0) {
fromIndex = 0;
}
if (tlen == 0) {
return fromIndex;
}
if (slen == 0) {
return -1;
}
int i = fromIndex;
int j = 0;
while (i <= slen && j <= tlen) {
/* cycle compare */
if (s[i] == t[j]) {
++i;
++j;
} else {
/* point back last position */
i = i - j + 1;
j = 0;
}
}
if (j > tlen) {
/* found target string retun first index position*/
return i - j;
} else {
/* can't find target string and retun -1 */
return -1;
}
}
3.2.3 練習:反轉字符串
class Solution {
public String reverseString(String s) {
final char[] array = s.toCharArray();
final int length = array.length;
for (int i = 0; i < length / 2; i++) {
char temp = array[i];
array[i] = array[length - i-1];
array[length - i-1] = temp;
}
return new String(array);
}
}
3.2.3 練習:反轉字符串裏的單詞
public String reverseWords(String s) {
String[] words = s.split(" ");
StringBuilder sb = new StringBuilder();
for(String word : words) {
sb.append(swapWord(0, word.length()-1, word.toCharArray())).append(" ");
}
return sb.toString().trim();
}
public String swapWord(int s, int e, char[] c) {
if(s >= e) {
return String.valueOf(c);
}
char temp = c[s];
c[s] = c[e];
c[e] = temp;
return swapWord(s+1, e-1, c);
}
3.2.3 練習:字符串轉換整數(stoi)
public int myAtoi(String str) {
//去除掉前後的空格
String strr = str.trim();
//存儲最終過濾出來的字符串
String strrr = null;
//字符串不爲空時並且字符串不全是空白字符串時才轉換
if(strr != null && strr.isEmpty() == false){
char f = strr.charAt(0);
//判斷字符串中的第一個非空格字符是不是一個有效整數字符
if(f >= '0' && f <= '9' || f == '+'|| f == '-'){
strrr = strr.substring(0,1); // 把第一位放進去(只能是數字、正負號)
//這時候循環只要數字,因爲正負號只能出現在第一位
for(int i = 1; i<strr.length();i++){
if(strr.charAt(i) >= '0' && strr.charAt(i) <= '9'){
strrr = strr.substring(0,i+1);
}
//這是遇到不符合要求的字符,直接忽略剩餘元素
else{break;}
}
}
}
//判斷最終字符串是否爲空或則只有一個正負號
if(strrr == null || strrr.equals("+") || strrr.equals("-"))
//此時strrr是String對象,如果使用==比較則比較的時內存地址
return 0;
//最終轉換成的數字
int num = 0;
//使用異常機制打印結果
try{
num = Integer.parseInt(strrr);
}catch (Exception e){
if(strrr.charAt(0) == '-')
return Integer.MIN_VALUE;
return Integer.MAX_VALUE;
}
return num;
}
參考文章:
散列表參考文章:
https://blog.csdn.net/ynnusl/article/details/89343419
https://blog.csdn.net/u012124438/article/details/78230478
字符串參考文章:
https://www.cnblogs.com/lcchuguo/p/5194323.html