目錄
今晚不想寫公司項目了,頭暈暈的,整理下數據結構吧:
數據結構:簡單說就是指一組數據的存儲結構,算法就是操作數據的方法。
首先,需要明白數據結構的繼承關係,數據結構一切都源於Collection接口和Map接口~
Collection繼承接口Iterable:顧名思義迭代,該接口只是返回了迭代器對象Iterator<T> iterator();接下來就可以通過iterator的hasNext方法對list進行迭代了。
數據結構包括:數組、鏈表、棧、隊列、散列表、二叉樹、堆、調錶、圖、樹;
算法包括:遞歸、排序、查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規劃、字符串匹配算法。
說下繼承關係List--->Collection-->Iterable
接口Iterable:顧名思義迭代,該接口只是返回了迭代器對象Iterator<T> iterator();接下來就可以通過iterator的hasNext方法對list進行迭代了。
一,數組
1,數組分配在一塊連續的數據空間上,元素有序可重複,所以在分配空間時必須要確定它的大小。
2,優點是查詢快,因爲有角標/索引;缺點是增刪慢,因爲空間是固定的,增刪需要複製數組中的元素。
3,用到的最多的是數組容器ArrayList,方法有:add(Object o),addAll(Object o),boolean contains(Object o),size(),remove()可傳item,index等,clear(),indexOf(),lastIndexOf();
4,add方法和remove方法:
先擴容,再賦值。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 數組賦值操作
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//1,先判斷elementData的內容是否爲空,如果是空的,就設置一個最小容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//最小容量是默認值(10)和size + 1中的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//2,開始擴容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果數組的新容量大於當前數據的大小,就需要擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//擴容函數
private void grow(int minCapacity) {
// 1.5倍擴容
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 複製數組元素
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意,當在指定位置 i 插入元素時,同樣會擴容,並且位置i之後的元素都要通過複製的方法依次後移。所以增刪效率低。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
return oldValue;
}
5,get方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
4,平時會用到的:
數組轉ArrayList:
ArrayList<Integer> arrayList = (ArrayList<Integer>) Arrays.stream(nums).boxed().collect(Collectors.toList());
二,鏈表
1,鏈表是一塊動態的空間,可以隨意的改變長短,初始化時不需要確定大小。由一系列結點(鏈表中每一個元素稱爲結點)組成,每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個(上一個)結點地址的指針域。
2,增刪快,因爲只需要修改指針,不需要移動複製元素;查詢慢,因爲沒有角標,只能從第一個元素開始查。
3,LinkedList:
數據結構是一個雙向鏈表,元素有序可重複,內部結構有前驅和後繼,不是連續的。
如圖:兩個指針一個指向前一個元素,一個指向後一個元素,單鏈表比這個少了一個指向前一個元素的指針。
private static class Node<E> {
E item;// 數據
Node<E> next;// 前一個節點
Node<E> prev;// 後一個節點
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
方法有add(),contains(),remove,push(進棧),pop(出站)
首先看add方法和remove方法:
public boolean add(E e) {
//連接最後一個
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
//創建一個節點,頭指針指向last,後指針爲空
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//如果last爲空說明鏈表是空的,說明新增的節點是頭結點;
//如果非空則把之前的last的後指針指向該節點。
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
public E remove(int index) {
// 檢查index是否爲0或是否越界
checkElementIndex(index);
return unlink(node(index));
}
//根據index查到對應的節點
Node<E> node(int index) {
// size >> 1其實就是size/2
//如果index小於size/2,則從鏈表的第一個節點開始遍歷到index然後返回節點;
//如果index大於size/2,則從鏈表最後一個節點開始倒敘遍歷到index然後返回節點。
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//斷開節點
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//先判斷是不是頭結點,是的話那下個節點作爲頭結點;
//不是的話前一個節點的尾指針就指向後一個節點,這個節點前指針指向空
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//判斷是不是尾結點,是的話那前一個節點作爲尾結點;
//不是的話那後一個節點的前指針指向前一個節點,這個節點的後指針指向空
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
再看下get方法:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
三,Map
參考鏈接:https://blog.csdn.net/zhengwangzw/article/details/104889549
1,hashMap的原理
HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表/紅黑樹的結合體,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。數組默認長度是16,負載因子是0.75,當元素的個數大於數組長度*0.75時,數組會擴容。如果初始化時傳入了初始大小k,那數組的長度就爲 大於k的那個2的整數次方。數組用來確定桶的位置。
爲什麼採用紅黑樹:
因爲鏈表較多時,時間複雜度最壞爲O(lgn),但鏈表爲O(n)。
HashMap是基於hashing的原理,使用put(k,v)存儲對象到HashMap中,使用get(k)從HashMap中獲取對象。
2,hashMap的put方法
當put時會先對key調用hashCode()方法,得到hash值後會通過hash&(length-1)得到這個元素在數組中的下標index(桶的位置);
如果發生了hash碰撞(計算得到的hash值相等,也就是該位置已經有元素了),那就繼續判斷key是否相等,相等的話就覆蓋,不相等的話那就放到鏈表最後(Java1.8前的版本是放到最前)。如果拉鍊過長,查詢的性能會下降,所以在Java8中添加了紅黑樹結構,當一個桶的長度超過8時,就將其轉爲紅黑樹鏈表,如果小於6,再重新轉換爲普通鏈表。
當get時,HashMap會使用key對象的hashCode找到在數組中的位置(桶的位置),然後調用keys.equals()方法去找到在鏈表中的正確節點,最終找到值。
3,hash函數怎麼設計的?
hash函數是先拿到key的hashcode,是32位的int值,然後讓hashcode的高16位和低16位進行異或操作,這樣設計可以儘可能降低hash碰撞,越分散越好,再就是位運算比較高效。
static final int hash(Object key){
int h;
//key爲null時hash爲0
return (key == null) ? 0 : (h=key.hashCode()) ^ (h>>>16);
}
4,爲什麼高16位和低16位異或能降低hash碰撞?爲什麼不能直接採用hashCode?
因爲key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。int值範圍爲-2147483648~2147483647(2的31次方),只要哈希函數映射的比較均勻鬆散,一般不會出現碰撞的,但是對內存來說,這麼大的數組是放不下的。所以採用了取模運算:
bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
//與運算比%要高效
return h & (length-1);
}
順便說一下,這也正好解釋了爲什麼HashMap的數組長度要取2的整數冪。因爲這樣(數組長度-1)正好相當於一個“低位掩碼”。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。
看下圖:
第一行取得了key的hashcode;
第二行進行高16位和低16位的異或運算(相同爲0,不同爲1),這樣前16位肯定都爲1,這樣就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來;
第三行進行(n-1)和hash的與運算(都爲1才爲1,其餘情況爲0),這裏n表示默認初始長度16.
5,Hashtable 與 HashMap 的簡單比較
HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減少實現此接口所需的工作。
HashMap 的 key 和 value 都允許爲 null,而 Hashtable 的 key 和 value 都不允許爲 null。HashMap 遇到 key 爲 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap則不是。Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現。所以有人一般都建議如果是涉及到多線程同步時採用 HashTable,沒有涉及就採用 HashMap。
除了HashTable,還可以解決線程不安全的方法有:在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的 Map 對象,並把它作爲一個封裝的對象來返回。也可以使用ConcurrentHashMap,ConcurrentHashMap成員變量使用volatile 修飾,免除了指令重排序,同時保證內存可見性,另外使用CAS操作和synchronized結合實現賦值操作,多線程操作只會鎖住當前操作索引的節點,如線程A只鎖住A節點所在的鏈表,線程B智鎖珠B節點所在的鏈表,互不干擾,這也叫分段鎖。
6,HashMap有序嗎?
因爲hash值是隨機插入的,所以無序!但LinkedHashMap是有序的!TreeMap也是有序的!
LinkedHashMap內部維護了一個單鏈表,有頭尾節點,同時LinkedHashMap節點Entry內部除了繼承HashMap的Node屬性,還有before 和 after用於標識前置節點和後置節點。可以實現按插入的順序或訪問順序排序。
TreeMap是按照Key的自然順序或者Comprator的順序進行排序,內部是通過紅黑樹來實現。所以要麼key所屬的類實現Comparable接口,或者自定義一個實現了Comparator接口的比較器,傳給TreeMap用戶key的比較。
四, Set
Set:無序(存入和取出的順序不一定相同),不能重複,非線程安全的。直接繼承Collection
HashSet :
數據結構是哈希表,存儲的元素時哈希值,常數階。
HashSet內部使用HashMap的key存儲元素,以此來保證元素不重複;
HashSet是無序的,因爲HashMap的key是無序的;
HashSet中允許有一個null元素,因爲HashMap允許key爲null;
若保證不添加相同元素,需重寫對象(如Person類)的兩個方法:hasncode(),equals()。
方法有:add,remove(Object o),contain(Object o),iteartor(),size(),isEmpty(),clear(),注意,沒有get方法!!
五,Tree
樹:樹的數據元素間具有層次關係的非線性的結構。
前序遍歷:中左右;後序遍歷:左右中;
二叉樹:
BinaryNode(T data , BinaryNode<T> left , BinaryNode<T> right )
TreeSet :
數據結構是樹 對數階log(n) 。
若保證不添加相同元素,元素的類(如Person類)需實現Comparable接口,並重寫compareTo方法。
Map:key-value 鍵值對,key不能重複,value可以重複,通過key可以獲得value
HashMap:數據結構是hashTable。
HashTree:樹結構,按照key值排序,但key的類需實現Comparable接口