20172328《程式設計與資料結構》實驗二:樹
20172328《程式設計與資料結構》實驗二:樹
- 課程:《軟體結構與資料結構》
- 班級: 1723
- 姓名: 李馨雨
- 學號:20172328
- 實驗教師:王志強老師
- 實驗日期:2018年11月5日-2018年11月12日
必修選修: 必修
一、實驗要求內容
- 實驗1:實現二叉樹
- 參考教材p212,完成鏈樹LinkedBinaryTree的實現(getRight,contains,toString,preorder,postorder)
用JUnit或自己編寫驅動類對自己實現的LinkedBinaryTree進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊 - 實驗2:中序先序序列構造二叉樹
- 基於LinkedBinaryTree,實現基於(中序,先序)序列構造唯一一棵二㕚樹的功能,比如給出中序HDIBEMJNAFCKGL和後序ABDHIEJMNCFGKL,構造出附圖中的樹,用JUnit或自己編寫驅動類對自己實現的功能進行測試,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊
- 實驗3:決策樹
- 自己設計並實現一顆決策樹,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
- 實驗4:表示式樹
- 輸入中綴表示式,使用樹將中綴表示式轉換為字尾表示式,並輸出字尾表示式和計算結果(如果沒有用樹,則為0分),提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
- 實驗5:二叉查詢樹
- 完成PP11.3,提交測試程式碼執行截圖,要全屏,包含自己的學號資訊,課下把程式碼推送到程式碼託管平臺
- 實驗6 : 紅黑樹分析
參考本部落格:點選進入對Java中的紅黑樹(TreeMap,HashMap)進行原始碼分析,並在實驗報告中體現分析結果。
二、實驗過程及結果
- 實驗1:實現二叉樹的解決過程及結果
實驗2:中序先序序列構造二叉樹的解決過程及結果
實驗3:決策樹的解決過程及結果
實驗4:表示式樹的解決過程及結果
實驗5:二叉查詢樹的解決過程及結果
實驗6 : 紅黑樹分析的解決過程及結果
寫在前面:剛找到TreeMap和HashMap的原始碼,其實是有些慌張不知所措的,靜下心來看一看,發現其實是對很多方法的註釋很長,所以兩個原始碼都是很長。
首先,我們先要去了解Map是啥?Key是啥?而Value又是啥?
在陣列中我們是通過陣列下標來對其內容索引的,而在Map中我們通過物件來對物件進行索引,用來索引的物件叫做key,其對應的物件叫做value。這就是平時說的鍵值對Key - value。
- HashMap和TreeMap最本質的區別:
- HashMap通過hashcode方法對其內容進行快速查詢,而 TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap,因為HashMap中元素的排列順序是不固定的。
- HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。HashMap繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。HashMap的實現不是同步的,這意味著它不是執行緒安全的。它的key、value都可以為null。此外,HashMap中的對映不是有序的。在HashMap中通過get()來獲取value,通過put()來插入value,ContainsKey()則用來檢驗物件是否已經存在。可以看出,和ArrayList的操作相比,HashMap除了通過key索引其內容之外,別的方面差異並不大。
TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。TreeMap繼承於AbstractMap,所以它是一個Map,即一個key-value集合。TreeMap實現了NavigableMap介面,意味著它支援一系列的導航方法。比如返回有序的key集合。TreeMap實現了Cloneable介面,意味著它能被克隆。TreeMap實現了java.io.Serializable介面,意味著它支援序列化。TreeMap基於紅黑樹(Red-Blacktree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。- HashMap的原始碼分析
- HashMap的建構函式
// 預設建構函式。
HashMap()
// 指定“容量大小”的建構函式
HashMap(int capacity)
// 指定“容量大小”和“載入因子”的建構函式
HashMap(int capacity, float loadFactor)
// 包含“子Map”的建構函式
HashMap(Map<? extends K, ? extends V> map)
- 關於HashMap建構函式的理解:
- HashMap遵循集合框架的約束,提供了一個引數為空的建構函式與有一個引數且引數型別為Map的建構函式。除此之外,還提供了兩個建構函式,用於設定HashMap的容量(capacity)與平衡因子(loadFactor)。
- 從程式碼上可以看到,容量與平衡因子都有個預設值,並且容量有個最大值
- 預設的平衡因子為0.75,這是權衡了時間複雜度與空間複雜度之後的最好取值(JDK說是最好的),過高的因子會降低儲存空間但是查詢(lookup,包括HashMap中的put與get方法)的時間就會增加。
- HashMap的繼承關係
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
- 關於HashMap繼承和實現的理解:
- 標記介面Cloneable,用於表明HashMap物件會重寫java.lang.Object#clone()方法,HashMap實現的是淺拷貝(shallow copy)。
- 標記介面Serializable,用於表明HashMap物件可以被序列化
- HashMap是一種基於雜湊表(hash table)實現的map,雜湊表(也叫關聯陣列)一種通用的資料結構,大多數的現代語言都原生支援,其概念也比較簡單:key經過hash函式作用後得到一個槽(buckets或slots)的索引(index),槽中儲存著我們想要獲取的值.
- HashMap的一些重要物件和方法
- HashMap中存放的是HashMap.Entry物件,它繼承自Map.Entry,其比較重要的是建構函式。Entry實現了單向連結串列的功能,用next成員變數來級連起來。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
// setter, getter, equals, toString 方法省略
public final int hashCode() {
//用key的hash值與上value的hash值作為Entry的hash值
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
- HashMap內部維護了一個為陣列型別的Entry變數table,用來儲存新增進來的Entry物件。其實這是解決衝突的一個方式:鏈地址法(開雜湊法)
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
- get操作
public V get(Object key) {
//單獨處理key為null的情況
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
//key為null的Entry用於放在table[0]中,但是在table[0]衝突鏈中的Entry的key不一定為null
//所以需要遍歷衝突鏈,查詢key是否存在
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//首先定位到索引在table中的位置
//然後遍歷衝突鏈,查詢key是否存在
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
- put操作(含update操作)
private void inflateTable(int toSize) {
//輔助函式,用於填充HashMap到指定的capacity
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//threshold為resize的閾值,超過後HashMap會進行resize,內容的entry會進行rehash
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
//這裡的迴圈是關鍵
//當新增的key所對應的索引i,對應table[i]中已經有值時,進入迴圈體
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷是否存在本次插入的key,如果存在用本次的value替換之前oldValue,相當於update操作
//並返回之前的oldValue
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果本次新增key之前不存在於HashMap中,modCount加1,說明結構改變了
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果增加一個元素會後,HashMap的大小超過閾值,需要resize
if ((size >= threshold) && (null != table[bucketIndex])) {
//增加的幅度是之前的1倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//首先得到該索引處的衝突鏈Entries,有可能為null,不為null
Entry<K,V> e = table[bucketIndex];
//然後把新的Entry新增到衝突鏈的開頭,也就是說,後插入的反而在前面(第一次還真沒看明白)
//需要注意的是table[bucketIndex]本身並不儲存節點資訊,
//它就相當於是單向連結串列的頭指標,資料都存放在衝突鏈中。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//下面看看HashMap是如何進行resize,廬山真面目就要揭曉了
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果已經達到最大容量,那麼就直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//initHashSeedAsNeeded(newCapacity)的返回值決定了是否需要重新計算Entry的hash值
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍歷當前的table,將裡面的元素新增到新的newTable中
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
//最後這兩句用了與put放過相同的技巧
//將後插入的反而在前面
newTable[i] = e;
e = next;
}
}
}
/**
* Initialize the hashing mask value. We defer initialization until we
* really need it.
*/
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//這裡說明了,在hashSeed不為0或滿足useAltHash時,會重算Entry的hash值
//至於useAltHashing的作用可以參考下面的連結
// http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
- remove操作
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
//可以看到刪除的key如果存在,就返回其所對應的value
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
//這裡用了兩個Entry物件,相當於兩個指標,為的是防治衝突鏈發生斷裂的情況
//這裡的思路就是一般的單向連結串列的刪除思路
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
//當table[i]中存在衝突鏈時,開始遍歷裡面的元素
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e) //當衝突鏈只有一個Entry時
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
- TreeMap的原始碼分析
- TreeMap的建構函式
// 預設建構函式。使用該建構函式,TreeMap中的元素按照自然排序進行排列。
TreeMap()
// 建立的TreeMap包含Map
TreeMap(Map<? extends K, ? extends V> copyFrom)
// 指定Tree的比較器
TreeMap(Comparator<? super K> comparator)
// 建立的TreeSet包含copyFrom
TreeMap(SortedMap<K, ? extends V> copyFrom)
- TreeMap的繼承關係
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
- 關於TreeMap繼承和實現的理解:
- TreeMap實現繼承於AbstractMap,並且實現了NavigableMap介面。
- TreeMap的本質是R-B Tree(紅黑樹),它包含幾個重要的成員變數: root, size, comparator。
- root是紅黑數的根節點。它是Entry型別,Entry是紅黑數的結點,它包含了紅黑數的6個基本組成成分:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)。Entry結點根據key進行排序,Entry節點包含的內容為value。
- 紅黑數排序時,根據Entry中的key進行排序;Entry中的key比較大小是根據比較器comparator來進行判斷的。size是紅黑數中結點的個數。
- TreeMap的一些重要方法:
- 是否包含key結點:
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
- 是否包含某個值:
public boolean containsValue(Object value) {
for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
if (valEquals(value, e.value))
return true;
return false;
}
- 返回某一TreeMap上的value值
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
- 將某一個特定的Map存入TreeMap並進行自動排序
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
Comparator<?> c = ((SortedMap<?,?>)map).comparator();
if (c == comparator || (c != null && c.equals(comparator))) {
++modCount;
try {
buildFromSorted(mapSize, map.entrySet().iterator(),
null, null);
} catch (java.io.IOException | ClassNotFoundException cannotHappen) {
}
return;
}
}
super.putAll(map);
}
- 返回某一個結點
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
- 然後TreeMap中就是NavigableMap API 的方法,SubMaps、public Methods、View classes、Red-black mechanics了。
三、實驗過程中遇到的問題和解決過程
- 問題1:首先是在做實驗1的時候自己不能完整的輸出一個數,只能輸出包含樹根(有三個數)的二叉樹,連線在左孩子上的左子樹無法正常輸出。
問題1的解決:通過詢問王文彬同學,他教我理解了我程式碼中存在的問題,其實是因為我構造了兩個子樹,但卻沒有連線在一起形成一個完整的樹,修改後將左子樹
left
加入整個樹的構造中就可以了。
問題2:在實驗二我理解完前序輸出和中序輸出的奧妙之後,終於在苦苦的編碼戰鬥中寫完了程式時,測試一下,結果卻很適合給學長學姐們國考加油!
解決過程:看圖說話
- 問題3:在實驗三中我的決策樹被我獨具匠心的寫成了一個穿搭教程。但是但是在做的時候還是出現了BUG,當時我記成了Y是通向左子樹、N是通向右子樹,所以我出現了前言不接後語的問題,當時因為記反了但自己又不知道找了好久的問題。還有就是我當時多加了兩個回答語句體,在讀檔案的時候我把新連結的子樹順序放到了最後,結果出現了問題跳躍,不能銜接。
通過認真的多次研究修改,終於我的決策樹完美出道了。
- 問題4和問題4的解決:在做實驗4的時候沒有思路,看了郭愷同學的程式碼理解了一些,是建立了兩個棧,兩個棧中儲存的資料型別分別是String和樹型別,Sring型別來解決操作符,樹型別的來解決運算元。
其他(感悟、思考等)
我覺得很多東西理解和程式碼實現不是一回事,理解了我也不知道如何精確下手,但是在編寫的時候我又能更深刻的理解好多遍。雖然過程及其“撕心裂肺”,但是還是要多多受虐,才能在下次受虐的時候減輕疼痛。