⑴ HashMap源碼分析
HashMap(jdk 1.8)
使用哈希表存儲鍵值對,底層是一個存儲Node的table數組。其基本的運行邏輯是,table數組的每一個元素是一個槽,用於存儲Node對象,向Hash表中插入鍵值對時,首先計算鍵的hash值,並對數組容量(即數組長度)取余,定位該鍵值對應放在哪個槽中,若槽中已經存在元素(即哈希沖突),將Node節點插入到沖突鏈表尾端,當該槽中結點超過一定數量(默認為8)並且哈希表容量超過一定值(默認為64)時,對該鏈表進行樹化。低於一定數量(默認為6)後進行resize時,樹形結構又會鏈表化。當哈希表的總節點數超過閾值時,對哈希表進行擴容,容量翻倍。
由於哈希表需要用到key的哈希函數,因此對於自定義類作為key使用哈希表時,需要重寫哈希函數,保證相等的key哈希值相同。
由於擴容或樹化過程Node節點的位置會發生改變,因此哈希表是無序的,不同時期遍歷同一張哈希表,得到的節點順序會不同。
成員變數
Hash表的成員變數主要有存儲鍵值對的table數組,size,裝載因子loadFactory,閾值theshold,容量capacity。
table數組:
哈希表的實質就是table數組,它用來存儲鍵值對。哈希表中內部定義了Node類來封裝鍵值對。
Node類實現了Map的Entry介面。包括key,value,下一個Node指針和key的hash值。
容量Capacity:
HashMap中沒有專門的屬性存儲容量,容量等於table.lenth,即數組的長度,默認初始化容量為16。初始化時,可以指定哈希表容量,但容量必須是2的整數次冪,在初始化過程中,會調用tableSizeFor函數,得到一個不小於指定容量的2的整數次冪,暫時存入到threshold中。
注意,若容量設置過大,那麼遍歷整個哈希表需要耗費更多的時間。
閾值threshold:
指定當前hash表最多存儲多少個鍵值對,當size超過閾值時,hash表進行擴容,擴容後容量翻倍。
loadFactory:
threshold=capacity*loadFactory。
裝填因子的大小決定了哈希表存儲鍵值對數量的能力,它直接影響哈希表的查找性能。初始化時可以指定loadFactory的值,默認為0.75。
初始化過程
哈希表有3個構造函數,用戶可以指定哈希表的初始容量和裝填因子,但初始化過程只是簡單填入這兩個參數,table數組並沒有初始化。注意,這里的initialCapacity會向上取2的整數次冪並暫時保存到threshold中。
table數組的初始化是在第一次插入鍵值對時觸發,實際在resize函數中進行初始化。
hash過程與下標計算
哈希表是通過key的哈希值決定Node放入哪個槽中, 在java8中 ,hash表沒有直接用key的哈希值,而是自定義了hash函數。
哈希表中的key可以為null,若為null,哈希值為0,否則哈希值(一共32位)的高16位不變,低16位與高16位進行異或操作,得到新的哈希值。(h >>> 16,表示無符號右移16位,高位補0,任何數跟0異或都是其本身,因此key的hash值高16位不變。)
之所以這樣處理,是與table的下標計算有關
因為,table的長度都是2的冪,因此index僅與hash值的低n位有關(n-1為0000011111的形式),hash值的高位都被與操作置為0了,相當於hash值對n取余。
由於index僅與hash值的低n位有關,把哈希值的低16位與高16位進行異或處理,可以讓Node節點能更隨機均勻得放入哈希表中。
get函數:
V get(Object key) 通過給定的key查找value
先計算key的哈希值,找到對應的槽,遍歷該槽中所有結點,如果結點中的key與查找的key相同或相等,則返回value值,否則返回null。
注意,如果返回值是null,並不一定是沒有這種KV映射,也有可能是該key映射的值value是null,即key-null的映射。也就是說,使用get方法並不能判斷這個key是否存在,只能通過containsKey方法來實現。
put函數
V put(K key, V value) 存放鍵值對
計算key的哈希值,找到哈希表中對應的槽,遍歷該鏈表,如果發現已經存在包含該key的Node,則把新的value放入該結點中,返回該結點的舊value值(舊value可能是null),如果沒有找到,則創建新結點插入到 鏈表尾端 (java7使用的是頭插法),返回null。插入結點後需要判斷該鏈表是否需要樹化,並且判斷整個哈希表是否需要擴容。
由該演算法可以看出,哈希表中不會有兩個key相同的鍵值對。
resize函數
resize函數用來初始化或擴容table數組。主要做了兩件事。
1.根據table數組是否為空判斷初始化還是擴容,計算出相應的newCap和newThr,新建table數組並設置新的threshold。
2.若是進行擴容,則需要將原數組中的Node移植到新的數組中。
由於數組擴容,原來鍵值對可能存儲在新數組不同的槽中。但由於鍵值對位置是對數組容量取余(index=hash(key)&(Capacity-1)),由於Capacity固定為2的整數次冪,並新的Capacity(newCap)是舊的兩倍(oldCap)。因此,只需要判斷每個Node中的hash值在oldCap二進制那一位上是否為0(hash&oldCap==0),若為0,對newCap取余值與oldCap相同,Node在新數組中槽的位置不變,若為1,新的位置是就得位置加一個oldCap值。
因此hashMap的移植演算法是,遍歷舊的槽:
若槽為空,跳過。
若槽只有一個Node,計算該Node在新數組中的位置,放入新槽中。
若槽是一個鏈表,則遍歷這個鏈表,分別用loHead,loTail,hiHead,hiTail兩組指針建立兩個鏈表,把hash值oldCap位為0的Node節點放入lo鏈表中,hash值oldCap位為1的節點放入hi鏈表中,之後將兩個鏈表分別插入到新數組中,實現原鍵值對的移植。
lo鏈表放入新數組原來位置,hi鏈表放入新數組原來位置+oldCap中
樹化過程 :
當一個槽中結點過多時,哈希表會將鏈表轉換為紅黑樹,此處挖個坑。
[總結] java8中hashMap的實現原理與7之間的區別:
1.hash值的計算方法不同 2.鏈表採用尾插法,之前是頭插法 3.加入了紅黑樹結構
⑵ Mapbox源碼分析(2)url解析
通過源碼,我們來一步步分析Mapbox地圖引擎如何進行指定字元串變數解析成url地址載入的,這里是基於5.3.0的版本.
在官方demo中,我們不僅可以載入本地樣式文件,已定義樣式文件和網路在線文件,它們的格式分別是
1. "asset://test.json"
2 . "https://www.mapbox.com/android-docs/files/mapbox-raster-v8.json"
3 . "mapbox://styles/mapbox/streets-v10"
這些格式,那麼Mapbox如果解析這些字元串去獲取到需要的樣式數據呢?我們從 Mapbox源碼分析(1)樣式載入 這篇的loadURL()方法開始看起
我們在這里看到,樣式的數據是通過fileSource.request進行請求載入的,通過調試我們發現這個fileSource是FileSource的子類DefaultFileSource,那麼我們先看看這個DefaultFileSource是什麼時候傳進來的
我們在這里看到,是在構造方法時對fileSource變數進行初始化的,那麼我們只需要看到Style::Impl對象什麼時候構造的,便知道了fileSource的來源,繼續往回找
在這里我們發現Impl對象的fileSource是Style對象構造時傳進來的,那麼我們繼續往回找
這里我們看到Style對象是通過map.cpp里的getStyle對象獲取的,而style對象是在Map::Impl::Impl構造方法時初始化的,繼續往回找
這里我們其實也能大概猜出來Map::Impl對象是在Map構造方法時初始化的,那麼map對象又是什麼時候初始化的,是不是覺得很繞,馬上就快到了,我們找到native_map_view.cpp文件,發現在NativeMapView構造方法中構造了map對象
到這里我們已經基本清楚fileSource的來源了,是JAVA層NativeMapView對象初始化的時候傳下來的,我們繼續看到開頭,既然我們已經知道fileSource對象是DefaultFileSource,那麼它調用的request方法,也就是調用的DefaultFileSource的request方法,這里我們看到default_file_source.cpp文件
這里我們看到它轉到了它的實現類的request方法
這里我們可以看到根據url的不同,和載入方法的不同,將請求分別轉給了assetFileSource,localFileSource,onlineFileSource等的request方法,這里我們看onlineFileSource的request方法
看到這里我們看到根據請求的類型不同,去處理不同的url,在這些參數里我們看下apiBaseURL這個變數,這是一個base url,指定了伺服器地址,我們在constants.hpp文件中找到了它
constexpr const char* API_BASE_URL = "https://api.mapbox.com";
繼續往下看,我們選normalizeStyleURL()方法往下看
這里我們看到它先驗證了一下url,然後將url字元串包裝成URL對象,然後進行一個拼接成tpl變數,最後再通過transformURL函數進行一個轉換,這里我們先看它如何包裝這個URL對象的
這里我們看到它將字元串分解成query,scheme,domain,path四個變數進行存儲,我們再看看transformURL()函數
這里我們看到根據url的不同變數值進行了再次字元串拼接,甚至根據路徑的不同,繼續拆分成Path對象,最後將拼接結果返回,到這里有關url解析拼接的過程就講完了.
⑶ golang map源碼淺析
golang 中 map的實現結構為: 哈希表 + 鏈表。 其中鏈表,作用是當發生hash沖突時,拉鏈法生成的結點。
可以看到, []bmap 是一個hash table, 每一個 bmap是我們常說的「桶」。 經過hash 函數計算出來相同的hash值, 放到相同的桶中。 一個 bmap中可以存放 8個 元素, 如果多出8個,則生成新的結點,尾接到隊尾。
以上是只是靜態文件 src/runtime/map.go 中的定義。 實際上編譯期間會給它加料 ,動態地創建一個新的結構:
上圖就是 bmap的內存模型, HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,並不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding 欄位,節省內存空間。
每個 bmap設計成 最多隻能放 8 個 key-value 對 ,如果有第 9 個 key-value 落入當前的 bmap,那就需要再構建一個 bmap,通過 overflow 指針連接起來。
map創建方法:
我們實際上是通過調用的 makemap ,來創建map的。實際工作只是初始化了hmap中的各種欄位,如:設置B的大小, 設置hash 種子 hash 0.
注意 :
makemap 返回是*hmap 指針, 即 map 是引用對象, 對map的操作會影響到結構體內部 。
使用方式
對應的是下面兩種方法
map的key的類型,實現了自己的hash 方式。每種類型實現hash函數方式不一樣。
key 經過哈希計算後得到hash值,共 64 個 bit 位。 其中後B 個bit位置, 用來定位當前元素落在哪一個桶里, 高8個bit 為當前 hash 值的top hash。 實際上定位key的過程是一個雙重循環的過程, 外層循環遍歷 所有的overflow, 內層循環遍歷 當前bmap 中的 8個元素 。
舉例說明: 如果當前 B 的值為 5, 那麼buckets 的長度 為 2^5 = 32。假設有個key 經過hash函數計算後,得到的hash結果為:
外層遍歷bucket 中的鏈表
內層循環遍歷 bmap中的8個 cell
建議先不看此部分內容,看完後續 修改 map中元素 -> 擴容 操作後 再回頭看此部分內容。
擴容前的數據:
等量擴容後的數據:
等量擴容後,查找方式和原本相同, 不多做贅述。
兩倍擴容後的數據
兩倍擴容後,oldbuckets 的元素,可能被分配成了兩部分。查找順序如下:
此處只分析 mapaccess1 ,。 mapaccess2 相比 mapaccess1 多添加了是否找到的bool值, 有興趣可自行看一下。
使用方式:
步驟如下:
擴容條件 :
擴容的標識 : h.oldbuckets != nil
假設當前定位到了新的buckets的3號桶中,首先會判斷oldbuckets中的對應的桶有沒有被搬遷過。 如果搬遷過了,不需要看原來的桶了,直接遍歷新的buckets的3號桶。
擴容前:
等量擴容結果
雙倍擴容會將old buckets上的元素分配到x, y兩個部key & 1 << B == 0 分配到x部分,key & 1 << B == 1 分配到y部分
注意: 當前只對雙倍擴容描述, 等量擴容只是重新填充了一下元素, 相對位置沒有改變。
假設當前map 的B == 5,原本元素經過hash函數計算的 hash 值為:
因為雙倍擴容之後 B = B + 1,此時B == 6。key & 1 << B == 1, 即 當前元素rehash到高位,新buckets中 y 部分. 否則 key & 1 << B == 0 則rehash到低位,即x 部分。
使用方式:
可以看到,每一遍歷生成迭代器的時候,會隨機選取一個bucket 以及 一個cell開始。 從前往後遍歷,再次遍歷到起始位置時,遍歷完成。
https://www.qcrao.com/2019/05/22/dive-into-go-map/
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
https://www.bilibili.com/video/BV1Q4411W7MR?spm_id_from=333.337.search-card.all.click
⑷ 在OpenGL中,函數glMap2f具體怎麼用及如何確定參數如何確定控制點
int x = 5;
int y = 4;
int z = 3;
float u1=0;
float u2=1;
float v1=0;
float v2=1;
int n=100;
float data[x][y][z]={...};
Gl.glMap2f(Gl.GL_MAP2_VERTEX_3, u1, u2, z, y, v1, v2, z * y, x, &data[0][0][0]);
.
.
.
Gl.glMapGrid2f(n, u1, u2, n, v1, v2);
Gl.glEvalMesh2(Gl.GL_FILL, 0, n, 0, n);
大致帆粗巧就這樣態鍵 意凳源思一下