數組是一種線性數據結構。建立數組時會在內存中劃分出一塊連續的內存區域,數據會保存在這塊連續區域的每塊索引。java
首先說明經過下標獲取元素的時間複雜度爲O(1)。由於數組是一塊連續的內存區域,而且每一個元素的大小都相等,經過一個線性方程就很快能找到改下標對應的內存地址。例如若是是一個int數組,0位置的內存地址爲b,index是你要找的下標,那麼顯然你要獲取的內存地址就是b+4*index,4就是一個int佔的字節數。因此計算機經過b+type_size*index來計算下標。固然,並非全部的數組都是如此計算下標的,例若有的虛擬機在開闢數組空間時並非開闢的連續空間。node
增長時若是要指定增長到的數組下標通常將要對數組元素進行移動,例若有一個十個元素的整型數組,要將一個數字A插入到第一個元素的位置,那麼就須要先將全部元素從後往前向後移動一個位置再將A插入到第一個位置。因此數組的插入操做平均時間複雜度爲O(n)算法
和插入同樣,指定下標進行刪除。例若有十個元素,刪除第一個元素,那麼就須要將數組元素從當前元素由前至後向前移一個下標。因此數組的刪除操做平均時間複雜度爲O(n)數組
修改就比較簡單了,直接獲取下標改變當前元素的值便可瀏覽器
不少高級語言都有數組這個基本結構,可是在使用他們的時候若是咱們增長的元素超過一開始定義它的總個數的話是沒辦法繼續添加的。因此,當咱們一開始不知道這個數組的大小時這就比較麻煩了,咱們就須要本身定義動態數組,咱們不須要管他的初始容量。bash
實現動態數組主要須要重寫數組的增長、刪除操做,還要實現擴容操做數據結構
本身實現的增長操做和原來的區別就是要判斷是否須要將數組擴容。擴容的條件爲當數組的長度和元素的個數相同時就須要擴容,通常擴容爲兩倍。 擴容的步驟:app
本身實現的刪除操做和原來的區別就是要判斷是否須要將數組縮容,縮容是有必要的。縮容的條件比擴容的條件多一點,就是縮容前的數組大小要大於等於2,而且當前元素個數爲數組大小的一半。 縮容的步驟:函數
產生的震盪和咱們的擴容,縮容條件有關,若是按照上面的條件進行擴容和縮容,那麼若是這個數組的元素個數若是在8和9之間徘徊,那麼數組的大小就會在8,16中徘徊。測試
而震盪的解決方法和具體的需求有關,咱們能夠將縮容的條件改成"縮容前的數組大小要大於等於2,而且當前元素個數爲數組大小的1/4"
擴容確定在
//新增方法
public boolean add(E e) {
//modCount是記錄修改次數的(迭代器判斷結構是否變),迭代器fail-fast機制就靠它
modCount++;
//elementData是數組元素,size是數組大小
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
//數組大小和數組元素個數相等,要擴容了
if (s == elementData.length)
elementData = grow(s+1);
elementData[s] = e;
size = s + 1;
}
private Object[] grow(int minCapacity) {
//能夠看到也是經過複製一個新數組
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
//接下來就是如何真正實現擴容的!!!
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新大小是老大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//若是新大小比oldCapacity+1小
if (newCapacity - minCapacity <= 0) {
//若是數組是空的,第一次添加元素,就直接擴容到10和minCapacity中大的那個
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//返回大的那個,不超過最大
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
複製代碼
能夠看到擴容的思路仍是很簡單的,本人環境是jdk1.9,至於其餘版本的擴容思路我想都差很少。至於縮容,這裏就再也不闡述了,arraylist的源碼仍是比較容易理解的。
鏈表和數組都是線性結構,可是鏈表是一個一個的節點組成的,這個節點有一個next指針指向下一個節點,也就是說鏈表不須要連續的數組空間。如圖:
下面來看一下如何對鏈表進行增刪改查
增長的操做時間複雜度爲O(1),不用像數組同樣去移動數組元素,如A->C,增長B到A的後面 增長步驟:
刪除的時間複雜度爲O(1),不用像數組同樣去移動數組元素,如A->B->C,刪除B 刪除步驟:
查詢的時間複雜度爲O(n),由於鏈表不像數組能夠直接經過下標計算出內存地址。因此必須經過遍歷找到相應下標節點。
這裏的增長和刪除時間複雜度爲O(1)很好理解,咱們可能會得出一個結論那就是數組的查詢修改效率高,鏈表的刪除增長效率高。但實際上這也是分狀況的。當咱們在對一個節點進行插入或刪除時咱們要去遍歷到指定位置(由於咱們只有頭節點地址或者尾節點地址)。以前作過一個測試,對於java中的結構LinkedList(雙向鏈表)和ArrayList(雙向鏈表),在增長大概一百萬個元素的時候會發現數組的增長方法效率更高,由於在指定位置對鏈表添加元素鏈表要去遍歷。
虛擬頭節點:最後,通常鏈表都會有一個頭節點,這個節點指向鏈表的首節點,這個頭節點的用處是當咱們將刪除最後一個元素的時候不用專門判斷刪除只有一個元素的狀況
棧和隊列很類似,因此結合起來一塊兒看。
結構:數組array,top變量
數組用來保存進入的數據。top指針指向最後一個元素的下一個位置,若是棧爲空top指向0。
舉個例子:
結構:top指針,節點
如圖:
結構:虛擬頭節點,節點
如圖:
基於數組的隊列和基於數組的棧有一些不一樣。能夠想到,當數組隊列入隊時能夠增長到數組後面,出隊時將前面的元素移出,那麼就會出現問題就是前面出隊的元素會變爲不可用但又無法用,有一種解決方式能夠在出隊的時候把後面的元素放在前面,就像前面動態數組那樣刪除首元素便可,可是若是是這樣每出隊一次就總體移動一次元素未免也太耗時了一些。因此這裏引入循環隊列。
循環隊列要解決的問題就是數組隊列浪費空間的問題。循環隊列並非在物理地址上是循環的,而是在邏輯上循環的。
結構:數組,front指向頭,rear指向隊尾的後一位元素
front=front%array.length
如圖:
要注意的點
當咱們想在學校想找到某我的的信息,咱們會向教務處去查詢學號,而教務處得到你提供的學號就會給你一個學生的信息。這裏經過學號得到學生信息就是用了哈希表的思想,而學號和學生信息的對應關係就是哈希函數,而若是兩個學號對應到了同一個學生信息就是哈希衝突。
因此接下來來看一下哈希表中的名詞:
哈希表的查找時間複雜度爲O(1),哈希表查找的思路是直接經過關鍵字查找到元素。增長刪除的時間複雜度也是O(1),也就是說哈希表
底層有一個Node數組array,當前元素個數M
由於咱們要實現對於不一樣的key儘量的經過哈希函數得出不一樣的值。因此對於哈希函數的選取是比較重要的。
如下爲常見哈希函數:
接下來咱們將哈希函數都設置爲除留取餘法進行分析。
哈希衝突就是不一樣的兩個key,他們hash(key)以後獲得的結果相同。對於哈希衝突的解決也有多種
接下來咱們將哈希衝突解決方式設置爲鏈地址法進行分析。
例如用戶要增長一個K,V鍵值對,先將K的哈希值計算出來,再將哈希值對數組長度取模得到下標,若是下標沒有元素,就將K,V封裝成Node放入這個下標,若是下標已經有元素,就K,V封裝成Node加入到這個下標元素的最後面。
例如一個用戶要經過K查找這個節點,經過hash(K)再取模獲得這個數組的下標,若是這個下標沒有元素,表明查找失敗,若是這個下標有元素,那就從這個元素向後面的鏈表遍歷,有就返回。
刪除和修改邏輯相似
java中的HashMap是封裝的很是好的一個哈希表,經過分析它的實現也可讓咱們更完善本身的哈希表設計
咱們來看一下它有哪些關鍵字段
//負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//哈希表中的數組
transient Node<K,V>[] table;
//數組中每一個元素保存的下面這個節點
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//..一些節點其餘的方法
}
複製代碼
接下來來看一個put操做(增長操做)的流程。
//增長一個key,value
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//採用的hash算法
static final int hash(Object key) {
int h;
//能夠看到這裏hash算法用的key的hashCode和h右移16位的異或運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//最後調用的putVal操做
/**
* hash是傳來的哈希值
* key是鍵,value是值
* onlyIfAbsent爲false表明多個key會重寫
* evict爲false表明表處於建立狀態
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//若是一開始hashmap沒有元素的話初始化hashmap大小
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//沒有哈希衝突的話直接構造新的鏈表節點添加進數組中,i爲計算出的下標
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//接下來是有哈希衝突的狀況
else {
Node<K,V> e; K k;
//這裏計算出下標的節點的哈希值要等於以前傳過來計算好的哈希值。而且要引用同一個對象而且equals方法也要相等!!這裏表示判斷爲同一個對象的邏輯!!我曾經在這裏踩過坑。。其次,若是兩次的key都爲null的話
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//這裏不是同一個對象而且是TreeNode結構(當鏈表節點個數大於等於8的時候會轉化爲紅黑樹)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//接下來正常添加鏈表節點
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//若是value變換以後這裏會返回老的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
複製代碼
關於HashMap中的put操做,有如下幾點注意的地方
下一篇會總結樹結構數據類型,而且下一篇會把本身實現的數據類型分享出來