❶ 想問一下極客時間的編程課質量怎麼樣誰知道進來說說
極客時間的編程課質量很好呀!我當時編程課之前很詳細的了解了一下他們的課程內容,感覺內容豐富,涵蓋的知識面很廣,價格也合適,才選擇了極客時間。我報的是數據結構與演算法之美的課,那個授課的王爭老師是前Google工程師,經驗大大豐富,講課也很細致,學起來很輕松,真真是不錯哦!
❷ 演算法之美 隱匿在數據結構背後的原理 怎麼樣
還不錯吧。對於剛開始看演算法的人挺合適的,我上次去圖書館借了一本。因為以前看過一些演算法了,覺得挺好理解的。
❸ javase線程怎麼存儲到容器
Java 並發重要知識點
java 線程池
ThreadPoolExecutor 類分析
ThreadPoolExecutor 類中提供的四個構造方法。我們來看最長的那個,其餘三個都是在這個構造方法的基礎上產生(其他幾個構造方法說白點都是給定某些默認參數的構造方法比如默認制定拒絕策略是什麼)。
/**
* 用給定的初始參數創建一個新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量
int maximumPoolSize,//線程池的最大線程數
long keepAliveTime,//當線程數大於核心線程數時,多餘的空閑線程存活的最長時間
TimeUnit unit,//時間單位
BlockingQueue<Runnable> workQueue,//任務隊列,用來儲存等待執行任務的隊列
ThreadFactory threadFactory,//線程工廠,用來創建線程,一般默認即可
RejectedExecutionHandler handler//拒絕策略,當提交的任務過多而不能及時處理時,我們可以定製策略來處理任務
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
下面這些對創建非常重要,在後面使用線程池的過程中你一定會用到!所以,務必拿著小本本記清楚。
ThreadPoolExecutor 3 個最重要的參數:
corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。
workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。
ThreadPoolExecutor其他常見參數 :
keepAliveTime:當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷毀,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷毀;
unit : keepAliveTime 參數的時間單位。
threadFactory :executor 創建新線程的時候會用到。
handler :飽和策略。關於飽和策略下面單獨介紹一下。
下面這張圖可以加深你對線程池中各個參數的相互關系的理解(圖片來源:《Java 性能調優實戰》):
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nzbqGRz9-1654600571133)(https://javaguide.cn/assets/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%90%84%E4%B8%AA%E5%8F%82%E6%95%B0%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB.d65f3309.png)]
ThreadPoolExecutor 飽和策略定義:
如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任務時,ThreadPoolTaskExecutor 定義一些策略:
ThreadPoolExecutor.AbortPolicy :拋出 RejectedExecutionException來拒絕新任務的處理。
ThreadPoolExecutor.CallerRunsPolicy :調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程序的整體性能。如果您的應用程序可以承受此延遲並且你要求任何一個任務請求都要被執行的話,你可以選擇這個策略。
ThreadPoolExecutor.DiscardPolicy :不處理新任務,直接丟棄掉。
ThreadPoolExecutor.DiscardOldestPolicy : 此策略將丟棄最早的未處理的任務請求。
舉個例子:
Spring 通過 ThreadPoolTaskExecutor 或者我們直接通過 ThreadPoolExecutor 的構造函數創建線程池的時候,當我們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy。在默認情況下,ThreadPoolExecutor 將拋出 RejectedExecutionException 來拒絕新來的任務 ,這代表你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略為我們提供可伸縮隊列。(這個直接查看 ThreadPoolExecutor 的構造函數源碼就可以看出,比較簡單的原因,這里就不貼代碼了。)
推薦使用 ThreadPoolExecutor 構造函數創建線程池
在《阿里巴巴 Java 開發手冊》「並發處理」這一章節,明確指出線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
為什麼呢?
使用線程池的好處是減少在創建和銷毀線程上所消耗的時間以及系統資源開銷,解決資源不足的問題。如果不使用線程池,有可能會造成系統創建大量同類線程而導致消耗完內存或者「過度切換」的問題。
另外,《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險
Executors 返回線程池對象的弊端如下(後文會詳細介紹到):
FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
CachedThreadPool 和 ScheledThreadPool : 允許創建的線程數量為 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。
方式一:通過ThreadPoolExecutor構造函數實現(推薦)通過構造方法實現
方式二:通過 Executor 框架的工具類 Executors 來實現 我們可以創建三種類型的 ThreadPoolExecutor:
FixedThreadPool
SingleThreadExecutor
CachedThreadPool
對應 Executors 工具類中的方法如圖所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YGd4ygZu-1654600571136)(https://javaguide.cn/assets/Executors%E5%B7%A5%E5%85%B7%E7%B1%BB.4b0cbd16.png)]
正確配置線程池參數
說到如何給線程池配置參數,美團的騷操作至今讓我難忘(後面會提到)!
我們先來看一下各種書籍和博客上一般推薦的配置線程池參數的方式,可以作為參考!
常規操作
很多人甚至可能都會覺得把線程池配置過大一點比較好!我覺得這明顯是有問題的。就拿我們生活中非常常見的一例子來說:並不是人多就能把事情做好,增加了溝通交流成本。你本來一件事情只需要 3 個人做,你硬是拉來了 6 個人,會提升做事效率嘛?我想並不會。 線程數量過多的影響也是和我們分配多少人做事情一樣,對於多線程這個場景來說主要是增加了上下文切換成本。不清楚什麼是上下文切換的話,可以看我下面的介紹。
上下文切換:
多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 採取的策略是為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再載入這個任務的狀態。任務從保存到再載入的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
類比於實現世界中的人類通過合作做某件事情,我們可以肯定的一點是線程池大小設置過大或者過小都會有問題,合適的才是最好。
如果我們設置的線程池數量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執行,甚至會出現任務隊列滿了之後任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的! CPU 根本沒有得到充分利用。
但是,如果我們設置線程數量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執行時間,影響了整體執行效率。
有一個簡單並且適用面比較廣的公式:
CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是為了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閑狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。
I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。
如何判斷是 CPU 密集任務還是 IO 密集任務?
CPU 密集型簡單理解就是利用 CPU 計算能力的任務比如你在內存中對大量數據進行排序。但凡涉及到網路讀取,文件讀取這類都是 IO 密集型,這類任務的特點是 CPU 計算耗費時間相比於等待 IO 操作完成的時間來說很少,大部分時間都花在了等待 IO 操作完成上。
美團的騷操作
美團技術團隊在《Java線程池實現原理及其在美團業務中的實踐》open in new window這篇文章中介紹到對線程池參數實現可自定義配置的思路和方法。
美團技術團隊的思路是主要對線程池的核心參數實現自定義可配置。這三個核心參數是:
corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。
workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。
為什麼是這三個參數?
我在這篇《新手也能看懂的線程池學習總結》open in new window 中就說過這三個參數是 ThreadPoolExecutor 最重要的參數,它們基本決定了線程池對於任務的處理策略。
如何支持參數動態配置? 且看 ThreadPoolExecutor 提供的下面這些方法。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Sm89qdJZ-1654600571137)(https://javaguide.cn/assets/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.5ff332dc.png)]
格外需要注意的是corePoolSize, 程序運行期間的時候,我們調用 setCorePoolSize() 這個方法的話,線程池會首先判斷當前工作線程數是否大於corePoolSize,如果大於的話就會回收工作線程。
另外,你也看到了上面並沒有動態指定隊列長度的方法,美團的方式是自定義了一個叫做 的隊列(主要就是把LinkedBlockingQueue的capacity 欄位的final關鍵字修飾給去掉了,讓它變為可變的)。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cmNN5yAL-1654600571138)(https://javaguide.cn/assets/19a0255a-6ef3-4835-98d1-a839d1983332.b334d1e9.png)]
還沒看夠?推薦 why神的[《如何設置線程池參數?美團給出了一個讓面試官虎軀一震的回答。》open in new window](如何設置線程池參數?美團給出了一個讓面試官虎軀一震的回答。 (qq.com))這篇文章,深度剖析,很不錯哦!
Java 常見並發容器
JDK 提供的這些容器大部分在 java.util.concurrent 包中。
ConcurrentHashMap : 線程安全的 HashMap
CopyOnWriteArrayList : 線程安全的 List,在讀多寫少的場合性能非常好,遠遠好於 Vector。
ConcurrentLinkedQueue : 高效的並發隊列,使用鏈表實現。可以看做一個線程安全的 LinkedList,這是一個非阻塞隊列。
BlockingQueue : 這是一個介面,JDK 內部通過鏈表、數組等方式實現了這個介面。表示阻塞隊列,非常適合用於作為數據共享的通道。
ConcurrentSkipListMap : 跳錶的實現。這是一個 Map,使用跳錶的數據結構進行快速查找。
ConcurrentHashMap
我們知道 HashMap 不是線程安全的,在並發場景下如果要保證一種可行的方式是使用 Collections.synchronizedMap() 方法來包裝我們的 HashMap。但這是通過使用一個全局的鎖來同步不同線程間的並發訪問,因此會帶來不可忽視的性能問題。
所以就有了 HashMap 的線程安全版本—— ConcurrentHashMap 的誕生。
在 ConcurrentHashMap 中,無論是讀操作還是寫操作都能保證很高的性能:在進行讀操作時(幾乎)不需要加鎖,而在寫操作時通過鎖分段技術只對所操作的段加鎖而不影響客戶端對其它段的訪問。
CopyOnWriteArrayList
CopyOnWriteArrayList 簡介
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
在很多應用場景中,讀操作可能會遠遠大於寫操作。由於讀操作根本不會修改原有的數據,因此對於每次讀取都進行加鎖其實是一種資源浪費。我們應該允許多個線程同時訪問 List 的內部數據,畢竟讀取操作是安全的。
這和我們之前在多線程章節講過 ReentrantReadWriteLock 讀寫鎖的思想非常類似,也就是讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥。JDK 中提供了 CopyOnWriteArrayList 類比相比於在讀寫鎖的思想又更進一步。為了將讀取的性能發揮到極致,CopyOnWriteArrayList 讀取是完全不用加鎖的,並且更厲害的是:寫入也不會阻塞讀取操作。只有寫入和寫入之間需要進行同步等待。這樣一來,讀操作的性能就會大幅度提升。那它是怎麼做的呢?
CopyOnWriteArrayList 是如何做到的?
CopyOnWriteArrayList 類的所有可變操作(add,set 等等)都是通過創建底層數組的新副本來實現的。當 List 需要被修改的時候,我並不修改原有內容,而是對原有數據進行一次復制,將修改的內容寫入副本。寫完之後,再將修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀操作了。
從 CopyOnWriteArrayList 的名字就能看出 CopyOnWriteArrayList 是滿足 CopyOnWrite 的。所謂 CopyOnWrite 也就是說:在計算機,如果你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之後呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了。
CopyOnWriteArrayList 讀取和寫入源碼簡單分析
CopyOnWriteArrayList 讀取操作的實現
讀取操作沒有任何同步控制和鎖操作,理由就是內部數組 array 不會發生修改,只會被另外一個 array 替換,因此可以保證數據安全。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
CopyOnWriteArrayList 寫入操作的實現
CopyOnWriteArrayList 寫入操作 add()方法在添加集合的時候加了鎖,保證了同步,避免了多線程寫的時候會 出多個副本出來。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.Of(elements, len + 1);//拷貝新數組
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();//釋放鎖
}
}
ConcurrentLinkedQueue
Java 提供的線程安全的 Queue 可以分為阻塞隊列和非阻塞隊列,其中阻塞隊列的典型例子是 BlockingQueue,非阻塞隊列的典型例子是 ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞隊列或者非阻塞隊列。 阻塞隊列可以通過加鎖來實現,非阻塞隊列可以通過 CAS 操作實現。
從名字可以看出,ConcurrentLinkedQueue這個隊列使用鏈表作為其數據結構.ConcurrentLinkedQueue 應該算是在高並發環境中性能最好的隊列了。它之所有能有很好的性能,是因為其內部復雜的實現。
ConcurrentLinkedQueue 內部代碼我們就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞演算法來實現線程安全就好了。
ConcurrentLinkedQueue 適合在對性能要求相對較高,同時對隊列的讀寫存在多個線程同時進行的場景,即如果對隊列加鎖的成本較高則適合使用無鎖的 ConcurrentLinkedQueue 來替代。
BlockingQueue
BlockingQueue 簡介
上面我們己經提到了 ConcurrentLinkedQueue 作為高性能的非阻塞隊列。下面我們要講到的是阻塞隊列——BlockingQueue。阻塞隊列(BlockingQueue)被廣泛使用在「生產者-消費者」問題中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器為空時,消費者線程會被阻塞,直至隊列非空時為止。
BlockingQueue 是一個介面,繼承自 Queue,所以其實現類也可以作為 Queue 的實現來使用,而 Queue 又繼承自 Collection 介面。下面是 BlockingQueue 的相關實現類:
BlockingQueue 的實現類
下面主要介紹一下 3 個常見的 BlockingQueue 的實現類:ArrayBlockingQueue、LinkedBlockingQueue 、PriorityBlockingQueue 。
ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 介面的有界隊列實現類,底層採用數組來實現。
public class ArrayBlockingQueue<E>
extends AbstractQueue<E>
implements BlockingQueue<E>, Serializable{}
ArrayBlockingQueue 一旦創建,容量不能改變。其並發控制採用可重入鎖 ReentrantLock ,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。當隊列容量滿時,嘗試將元素放入隊列將導致操作阻塞;嘗試從一個空隊列中取一個元素也會同樣阻塞。
ArrayBlockingQueue 默認情況下不能保證線程訪問隊列的公平性,所謂公平性是指嚴格按照線程等待的絕對時間順序,即最先等待的線程能夠最先訪問到 ArrayBlockingQueue。而非公平性則是指訪問 ArrayBlockingQueue 的順序不是遵守嚴格的時間順序,有可能存在,當 ArrayBlockingQueue 可以被訪問時,長時間阻塞的線程依然無法訪問到 ArrayBlockingQueue。如果保證公平性,通常會降低吞吐量。如果需要獲得公平性的 ArrayBlockingQueue,可採用如下代碼:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
1
1
LinkedBlockingQueue
LinkedBlockingQueue 底層基於單向鏈表實現的阻塞隊列,可以當做無界隊列也可以當做有界隊列來使用,同樣滿足 FIFO 的特性,與 ArrayBlockingQueue 相比起來具有更高的吞吐量,為了防止 LinkedBlockingQueue 容量迅速增,損耗大量內存。通常在創建 LinkedBlockingQueue 對象時,會指定其大小,如果未指定,容量等於 Integer.MAX_VALUE 。
相關構造方法:
/**
*某種意義上的無界隊列
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
*有界隊列
* Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
PriorityBlockingQueue
PriorityBlockingQueue 是一個支持優先順序的無界阻塞隊列。默認情況下元素採用自然順序進行排序,也可以通過自定義類實現 compareTo() 方法來指定元素排序規則,或者初始化時通過構造器參數 Comparator 來指定排序規則。
PriorityBlockingQueue 並發控制採用的是可重入鎖 ReentrantLock,隊列為無界隊列(ArrayBlockingQueue 是有界隊列,LinkedBlockingQueue 也可以通過在構造函數中傳入 capacity 指定隊列最大的容量,但是 PriorityBlockingQueue 只能指定初始的隊列大小,後面插入元素的時候,如果空間不夠的話會自動擴容)。
簡單地說,它就是 PriorityQueue 的線程安全版本。不可以插入 null 值,同時,插入隊列的對象必須是可比較大小的(comparable),否則報 ClassCastException 異常。它的插入操作 put 方法不會 block,因為它是無界隊列(take 方法在隊列為空的時候會阻塞)。
推薦文章: 《解讀 Java 並發隊列 BlockingQueue》open in new window
ConcurrentSkipListMap
下面這部分內容參考了極客時間專欄《數據結構與演算法之美》open in new window以及《實戰 Java 高並發程序設計》。
為了引出 ConcurrentSkipListMap,先帶著大家簡單理解一下跳錶。
對於一個單鏈表,即使鏈表是有序的,如果我們想要在其中查找某個數據,也只能從頭到尾遍歷鏈表,這樣效率自然就會很低,跳錶就不一樣了。跳錶是一種可以用來快速查找的數據結構,有點類似於平衡樹。它們都可以對元素進行快速的查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整。而對跳錶的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高並發的情況下,你會需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,你只需要部分鎖即可。這樣,在高並發環境下,你就可以擁有更好的性能。而就查詢的性能而言,跳錶的時間復雜度也是 O(logn) 所以在並發數據結構中,JDK 使用跳錶來實現一個 Map。
跳錶的本質是同時維護了多個鏈表,並且鏈表是分層的,
2級索引跳錶
最低層的鏈表維護了跳錶內所有的元素,每上面一層鏈表都是下面一層的子集。
跳錶內的所有鏈表的元素都是排序的。查找時,可以從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的取值,就會轉入下一層鏈表繼續找。這也就是說在查找過程中,搜索是跳躍式的。如上圖所示,在跳錶中查找元素 18。
在跳錶中查找元素18
查找 18 的時候原來需要遍歷 18 次,現在只需要 7 次即可。針對鏈表長度比較大的時候,構建索引查找效率的提升就會非常明顯。
從上面很容易看出,跳錶是一種利用空間換時間的演算法。
使用跳錶實現 Map 和使用哈希演算法實現 Map 的另外一個不同之處是:哈希並不會保存元素的順序,而跳錶內所有的元素都是排序的。因此在對跳錶進行遍歷時,你會得到一個有序的結果。所以,如果你的應用需要有序性,那麼跳錶就是你不二的選擇。JDK 中實現這一數據結構的類是 ConcurrentSkipListMap。
❹ 數據結構和演算法好難啊,想在極客時間上報課學習,大家覺得怎麼樣
數據結構和演算法確實不容易學,裡面不僅僅涉及敬兆衡到了知識點,還有演算法思維。演算法思維這種東西沒人點撥很難學到精髓,而這恰恰又是想學好數據結構和演算法的必備技能,所以報猜罩班學習,讓行業大牛帶著學是最穩妥的。極客時間上面的課程內容質量高,授課老師都是大廠出身,亮做在程序員圈子裡面口碑很好,你可以放心報名。我的回答不知你是否滿意?
❺ 數據結構與演算法--堆和堆排序
堆排序是一種原地的、時間復雜度為 O(nlogn) 的排序演算法。
堆是一種特殊的樹。
只要滿足這兩點,它就是一個堆:
對於每個節點的值都大於等於子樹中每個節點值的堆,我們叫做 「大頂堆」 。對於每個節點的值都小於等於子樹中每個節點值的堆,我們叫做 「小頂堆」 。
完全二叉樹比較適合用數組來存儲。用數組來存儲完全二叉樹是非常節省存儲空間的。下標可以直接計算出左右字數的下標。(數組中下標為 i 的節點,左子節點下標為 i∗2 ,右子節點下標為 i∗2+1,父節點的下標為 i/2 。)
如果我們把新插入的元素放到堆的最後,你可以看我畫的這個圖,是不是不符合堆的特性了?於是,我們就需要進行調整,讓其重新滿足堆的特性,這個過程我們起了一個名字,就叫做 堆化(heapify) 。
堆化實際上有兩種,從下往上和從上往下。這里我先講從下往上的堆化方法。
堆化非常簡單,就是順著節點所在的路徑,向上或者向下,對比,然後交換。
我們把最後一個節點放到堆頂,然後利用同樣的父子節點對比方法。對於不滿足父子節點大小關系的,互換兩個節點,並且重復進行這個過程,直到父子節點之間滿足大小關系為止。這就是 從上往下的堆化方法 。
一個包含 n 個節點的完全二叉樹,樹的高度不會超過 log2n。堆化的過程是順著節點所在路徑比較交換的,所以堆化的時間復雜度跟樹的高度成正比,也就是 O(logn)。插入數據和刪除堆頂元素的主要邏輯就是堆化,所以,往堆中插入一個元素和刪除堆頂元素的時間復雜度都是 O(logn)。
這里我們藉助於堆這種數據結構實現的排序演算法,就叫做堆排序。這種排序方法的時間復雜度非常穩定,是 O(nlogn),並且它還是原地排序演算法。
從後往前處理數組,並且每個數據都是從上往下堆化。
因為葉子節點往下堆化只能自己跟自己比較,所以我們直接從最後一個非葉子節點開始,依次堆化就行了。
建堆的時間復雜度就是 O(n)。 推導過程見 極客時間--數據結構與演算法之美
建堆結束之後,數組中的數據已經是按照大頂堆的特性來組織的。數組中的第一個元素就是堆頂,也就是最大的元素。我們把它跟最後一個元素交換,那最大元素就放到了下標為 n 的位置。
這個過程有點類似上面講的「刪除堆頂元素」的操作,當堆頂元素移除之後,我們把下標為 n 的元素放到堆頂,然後再通過堆化的方法,將剩下的 n−1 個元素重新構建成堆。堆化完成之後,我們再取堆頂的元素,放到下標是 n−1 的位置,一直重復這個過程,直到最後堆中只剩下標為 1 的一個元素,排序工作就完成了。
整個堆排序的過程,都只需要極個別臨時存儲空間,所以堆排序是原地排序演算法。堆排序包括建堆和排序兩個操作,建堆過程的時間復雜度是 O(n),排序過程的時間復雜度是 O(nlogn),所以,堆排序整體的時間復雜度是 O(nlogn)。
堆排序不是穩定的排序演算法,因為在排序的過程,存在將堆的最後一個節點跟堆頂節點互換的操作,所以就有可能改變值相同數據的原始相對順序。
堆這種數據結構幾個非常重要的應用:優先順序隊列、求 Top K 和求中位數。
假設我們有 100 個小文件,每個文件的大小是 100MB,每個文件中存儲的都是有序的字元串。我們希望將這些 100 個小文件合並成一個有序的大文件。這里就會用到優先順序隊列。
這里就可以用到優先順序隊列,也可以說是堆。我們將從小文件中取出來的字元串放入到小頂堆中,那堆頂的元素,也就是優先順序隊列隊首的元素,就是最小的字元串。我們將這個字元串放入到大文件中,並將其從堆中刪除。然後再從小文件中取出下一個字元串,放入到堆中。循環這個過程,就可以將 100 個小文件中的數據依次放入到大文件中。
我們可以用優先順序隊列來解決。我們按照任務設定的執行時間,將這些任務存儲在優先順序隊列中,隊列首部(也就是小頂堆的堆頂)存儲的是最先執行的任務。
如何在一個包含 n 個數據的數組中,查找前 K 大數據呢?我們可以維護一個大小為 K 的小頂堆,順序遍歷數組,從數組中取出數據與堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理,繼續遍歷數組。這樣等數組中的數據都遍歷完之後,堆中的數據就是前 K 大數據了。
中位數,顧名思義,就是處在中間位置的那個數。
使用兩個堆:一個大頂堆, 一個小頂堆。 小頂堆中的數據都大於大頂堆中的數據。
如果新加入的數據小於等於大頂堆的堆頂元素,我們就將這個新數據插入到大頂堆;否則,我們就將這個新數據插入到小頂堆。
也就是說,如果有 n 個數據,n 是偶數,我們從小到大排序,那前 2n 個數據存儲在大頂堆中,後 2n 個數據存儲在小頂堆中。這樣,大頂堆中的堆頂元素就是我們要找的中位數。如果 n 是奇數,情況是類似的,大頂堆就存儲 2n+1 個數據,小頂堆中就存儲 2n 個數據。
極客時間--數據結構與演算法之美--28 | 堆和堆排序:為什麼說堆排序沒有快速排序快?
❻ 數據結構與演算法(Data structure and Algorithm)
數據結構是數據對象在計算機中的組織方式和及加在其上的一系列操作的總稱。
完成這些操作所用的方法就是演算法。
演算法(Algorithm):一個有限指令集、接受一些輸入、產生一些輸出、在有限的步驟之後終止,並且每一條指令應該是有明確的目標、無歧義,在賀橡計算機能處理的范圍。
解決問題方法的效率與數據的組織形式,空間的利用率,演算法的巧妙程散鏈度有關。
n是變數,是問題規模的意思。
空間復雜度沖拍孫S(n) :佔用存儲單元的長度,遞歸:S(n) = C*N
時間復雜度T(n):消耗時間的長度,遞歸:T(n)= n
最壞復雜度,平均復雜度
漸進表示法:
好的演算法是空間復雜度,時間復雜度都在合理范圍。
❼ 數據結構與演算法之美筆記——散列表(上)
摘要:
我們已經知道隨機訪問數組元素時間復雜度只有 ,效率極高,當我們想利用數組的這個特性時就需要將元素下標與存儲信息對應。例如,一個商店只有四件商品,依次編號 0 至 3,這樣就可以將四件商品信息按照編號對應下標的方式存儲到數組中,依據編號就可以快速從數組中找到相應商品信息。
如果一段時間之後,商店盈利並且重新進貨 100 件商品,商家想對大量商品在編號上區分類別,這時候需要使用類別編號加順序編號的方式標識每件商品,這種編號變得復雜,並不能直接對應數組下標,此時的商品編號又該如何對應數組下標以實現快速查找商品的功能?這時候我們可以將類別編號去除之後按照順序編號對應數組下標,同樣也能享受數組高效率隨機訪問的福利。這個例子中,商品編號稱為「 鍵 」或「 關鍵字 」,將鍵轉化為數組對應下標的方法就是「 散列函數 」或「 Hash 函數 」,由散列函數生成的值叫做「 散列值 」或「 Hash 值 」,而這樣的數組就是散列表。
從散列表的原理來看,數據通過散列函數計算得到散列值是關鍵,這個步驟中散列函數又是其中的核心,一個散列函數需要遵守以下三個原則。
因為散列函數生成的散列值對應數組下標,而數組下標就是非負整數,所以需要滿足第一個原則;兩個相等的數據經過散列演算法得到的散列值肯定相等,否則利用散列值在散列表中查找數據就無從談起;至於第三個原則雖然在情理之中,卻不那麼容易做到,即使是被廣泛運用的散列演算法也會出現散列值沖突的情況,導致無法滿足第三個原則。
散列函數作為散列表的核心部分,必然不能拖散列表的執行效率後腿,畢竟散列表的查詢、插入和刪除操作都需要經過散列函數,所以散列函數不能太復雜,執行效率不能太低。由於散列函數不可避免地都會出現散列沖突情況,散列函數要盡量降低散列沖突,使散列值能夠均勻地分布在散列表中。
解決散列沖突主要有「 開放定址 」(open addressing)和「 鏈表法 」(chaining)兩類方法。
開放定址法是指插入操作時,當生成的散列值對應槽位已經被其他數據佔用,就探測空閑位置供插入使用,其中探測方法又分為「 線性探測 」(Linear Probing)、「 二次探測 」(Quadratic Probing)和「 雙重散列 」(Double hashing)三種。
線性探測是其中較為簡單的一種,這種探測方式是當遇到散列沖突的情況就順序查找(查找到數組尾部時轉向數組頭部繼續查找),直到查找到空槽將數據插入。當進行查找操作時,也是同樣的操作,利用散列值從散列表中取出對應元素,與目標數據比對,如果不相等就繼續順序查找,直到查找到對應元素或遇到空槽為止,最壞情況下查找操作的時間復雜度可能會下降為 。
散列表除了支持插入和查找操作外,當然也支持刪除操作,不過並不能將需刪除的元素置為空。如果刪除操作是將元素置為空的話,查找操作遇到空槽就會結束,存儲在被刪除元素之後的數據就可能無法正確查找到,這時的刪除操作應該使用標記的方式,而不是使用將元素置空,當查找到被標識已刪除的元素將繼續查找,而不是就此停止。
線性探測是一次一個元素的探測,二次探測就是使用都是線性探測的二次方步長探測。例如線性探測是 ,那二次探測對應的就是 。
雙重探測是當第一個散列函數沖突時使用第二個散列函數運算散列值,利用這種方式探測。例如,當 沖突時,就使用 計算散列值,如果再沖突就使用 計算散列值,依此類推。
關於散列表的空位多少使用「 裝載因子 」(load factor)表示,裝載因子滿足數學關系 ,也就是說裝載因子越大,散列表的空閑空間越小,散列沖突的可能性也就越大,一般我們會保持散列表有一定比例的空閑空間。
為了保持散列表一定比例的空閑空間,在裝載因子到達一定閾值時需要對散列表數據進行搬移,但散列表搬移比較耗時。你可以試想下這樣的步驟,在申請一個新的更大的散列表空間後,需要將舊散列表的數據重新通過散列函數生成散列值,再存儲到新散列表中,想想都覺得麻煩。
散列表搬移的操作肯定會降低散列表的操作效率,那能不能對這一過程進行改進?其實可以將低效的擴容操作分攤至插入操作,當裝載因子達到閾值時不一次性進行散列表搬移,而是在每次插入操作時將一個舊散列表數據搬移至新散列表,這樣搬移操作的執行效率得到了提高,插入操作的時間復雜度也依然能保持 的高效。當新舊兩個散列表同時存在時查詢操作就要略作修改,需先在新散列表中查詢,如果沒有查找到目標數據再到舊散列表中查找。
當然,如果你對內存有更高效的利用要求,可以在裝載因子降低至某一閾值時對散列表進行縮容處理。
除了開放定址之外,還可以使用鏈表法解決散列沖突的問題。散列值對應的槽位並不直接存儲數據,而是將數據存儲在槽位對應的鏈表上,當進行查找操作時,根據散列函數計算的散列值找到對應槽位,再在槽位對應的鏈表上查找對應數據。
鏈表法操作的時間復雜度與散列表槽位和數據在槽位上的分布情況有關,假設有 n 個數據均勻分布在 m 個槽位的散列表上,那鏈表法的時間復雜度為 。鏈表法可以不用像開放定址一樣關心裝載因子,但需要注意散列函數對散列值的計算,使鏈表結點能夠盡可能均勻地分布在散列表槽位上,避免散列表退化為鏈表。有時黑客甚至會精心製造數據,利用散列函數製造散列沖突,使數據集中某些槽位上,造成散列表性能的極度退化。
面對這樣的惡意行為散列表只能坐以待斃嗎?其實不然,當槽位上的鏈表過長時,可以將其改造成之前學習過的跳錶等,鏈表改造為跳錶後查詢的時間復雜度也只是退化為 ,依然是可以接受的范圍。
鏈表法在存儲利用上比開放定址更加高效,不用提前申請存儲空間,當有新數據時申請一個新的結點就行。而且鏈表法對裝載因子也不那麼敏感,裝載因子的增高也只是意味著槽位對應的鏈表更長而已,鏈表增長也有將鏈表改造為跳錶等結構的應對策略,所以鏈表法在裝載因子超過 1 的情況下都可保持高效。
開放定址不存在像鏈表法一樣有鏈表過長而導致效率降低的煩惱,不過裝載因子是開放定址的晴雨表,裝載因子過高會造成散列沖突機率的上升,開放定址就需要不斷探測空閑位置,演算法的執行成本會不斷被提高。而且在刪除操作時只能將數據先標記為刪除,對於頻繁增刪的數據效率會受到影響。
當然也可以在這種風險出現前進行散列表的動態擴容,不過這樣就會出現大量空閑的存儲空間,導致存儲的利用效率過低,這種現象在數據量越大的情況下越明顯。所以開放定址比較適用於數據量較小的情況。
鏈表法對於散列沖突的處理更加靈活,同時對存儲空間的利用效率也更高,但鏈表結點除了存儲數據外還需要存儲指針,如果存儲數據較小指針佔用的存儲甚至會導致整體存儲翻倍的情況,但存儲數據較大時指針佔用的存儲也就可以忽略不計,所以鏈表法較適合存儲數據對象較大,但頻繁的增刪操作不會對鏈表法造成明顯的影響。因為這樣的特點,鏈表法更加適合大數據量,或者數據對象較大的時候,如果數據操作頻繁,那鏈表法更是不二之選。
散列表由數組擴展而來,使用散列函數將鍵計算為散列值,散列值對應數據存儲的數組下標。雖然散列表的執行效率較高,但會有散列沖突的問題,可以通過開放定址法和鏈表法解決此問題。
開放定址存儲利用效率較低,適用數據量較小並且增刪不頻繁的情況,如果數據量較大,增刪頻繁的情況更加適用鏈表法,相對之下鏈表法更加普適。
❽ 極客時間IT課程的老師實力怎麼樣課程的干貨多不多
那王爭、楊曉峰、丁奇等老師都是很有實力的!干貨很多的!王爭是前Google的工程師,我看了他的《數據結構和演算法之美》《設計模式之美》兩個專欄 ,數據結構和演算法是教你如何寫出高效的代碼,那設計模式講的就是如何寫出可擴展、可讀、可維護的高質量代碼。我感覺《設計模式之美》可以提高自己的開發能力,這可是實打實的硬核技能! 上面的課還是很推薦的。
❾ 數據結構和演算法優化
APP的優化是任重而道遠的過程,必須在意每一個環節,否者當你想要優化的時候,發現到處都是坑,已經不知道填補哪裡了,所以我們必須一點一滴的做起。
數據結構和演算法優化
能帶來什麼好處呢?他能使得你程序獲得數據更快,內存佔用更合理。最終體現為響應快內存佔用小。
我們先看常見的數據結構類型特點
數組 : 一片物理上連續的大小確定的儲存空間 。int[num]
順序表 :物理上連續、邏輯上連續、大小可以動態增加。ArrayList (查找快,添加刪除慢)
鏈表 :物理上不連續、邏輯上連續、可以動態增加和刪除節點。LinkedList (查找慢只能輪尋,增加刪除快)
物理上連續:數組或者鏈表在初始化的時候,會申請分配內存空間:只要存儲空間足夠你申請的大小就分配給你初始化(物理不連續);必須要連續的存儲空間,我才給你分配,否則失敗(物理上連續)
那麼有沒有繼承純虛標和鏈表的2個有點的數據結構呢?HashMap!
HashMap
它是由數組和鏈表結合組成。(HashMap:JDK1.7之前 24 之前: 數組+ 鏈表; HashMap:JDK1.8 之後: 數組+ 鏈表 + 紅黑樹)
下面是HashMap結構圖
它是怎麼操作的呢?為什麼他能同時擁有順序表和鏈表的優點呢? 搞清它的實現方式,我們就可以知道了, 大致可以分為以下的步驟。
①put方法,傳入object和value,通過hash運算得到一個int類型的hashcode,這里假設為X(後續X為這個hashcode)。
②hashmap內部是有一個table數組+鏈表形成的。我們拿到這個X後,使用X/table.length(hashcode值/table[].length),得到一個小於table.length的值M,該值就是這個value應該放置的數組位置。我們准備把value放入table[M]中。
③我們把hashcode和value打包為一個node節點(為什麼需要這么打包後續會提到),准備存入table[M]中。
④出入table數組的鏈表中有2種方式:
前插方式:不管數組table[M]節點有值與否,都把這個准備插入的node節點作為數組的根節點。可能出現2種情況:
(1)如果table[M]節點沒有值,則node節點作為數組的根節點。
(2)如果table[M]節點已存在數據鏈表,就把這些數據灶含鏈表,鏈到這個准備插入的node節點上,以弄得節點為根節點放入table[M中]。
後插方式:可能會出現的2種情況
碼清 (1) 如果table[M]節點沒有值,則node節點作為數組的根節點。
(2)如果table[M]節點已存在數據鏈表,則把node節點鏈到該數據鏈表的最後一個節點上。
經歷以上4個步驟就完成了hashmap的插入操作,現在解釋一下為什麼要打包為node節點。
舉個栗子,假如hashmap.length=16,我們准備存入ObjectA(OA)和ObjectB(OB),假設OA經過hash運算得到的hashcode是1,OB經過hash運算得到hashcode是17,OA和OB進行求模運算結果都為1,鏈到鏈表上時,我們get方法的時候怎麼取到正確的值呢,因為鏈表上的模運算都是1.這個時候我們就需要通過hashcode來識別這個鏈表上隱模笑的哪個值是OA的value哪個是OB的value,因為我們已經把hashcode和value打包起來了。
補充
hashmap的table數組的大小事是2的次冪(不要問為什麼,源碼定的,他們肯定經過大量的統計或者運算,這是科學)。table數組默認的長度是16,也就是說你new一個空的hashmap長度為16,當然也提供了一個給你設置長度的方法,但是假如你設置17,則長度會為32,這不難理解。
hash碰撞
hash碰撞就是,假如OA、OB...ON經過模運算得到的數組位置相同,那麼他們都會掛在這個數組節點的鏈表上,極端情況想整個hashmap看起來像單鏈表。但這種情況這並不是我們想要的結果。我們可以通過擴容來盡可能的避免hash碰撞。
擴容 :(意義,在於避免大量的hash碰撞,因為在某些極端情況下,有點像單鏈表)
閾值 :閾值=table.length* DEFAULT_LOAD_FACTOR (擴容系數,默認為0.75,也可以自己設定,一般不做修改)
hashmap定義:當hashmap中的元素個數超過閾值大小時,我們就需要對table數組進行2倍擴容,如從16→32。
注意:擴容後hashmap會調用resize(),對hashmap內的數據重新計算所有元素的位置 。 。因為假如你之前17/16=1,現在17/32=17,你的位置發生變化了。
缺點 :
hashMap因為有閾值的擴容機制,所以一定會有空間浪費,比如0.75的時候,一定有25%空間被浪費掉了。空間換時間。
hashmap是線程不安全的。因為可能在一個線程擴容(resize()方法執行)的情況下,另外一個線程在get,但是拿不到之前的數據了,因為擴容。所以是線程不安全的。或者線程擴容(resize()方法執行時,多線程進行put的時候導致的多線程數據不一致。
如何線程安全的使用HashMap?使用使用鎖分段技術或者使用HashTable(Hashtable的方法是Synchronize的,而HashMap不是,其實也就是鎖機制起作用)。
SparseArray(Android為了優化內存所提供的api)
特性:key為int,value為object,二分查找的思想,雙數組,刪除的時候節點不刪除,而是把value刪除,避免刪除的時候數組還要移動。
SparseArray比HashMap更省內存,在某些條件下性能更好,主要是因為它避免了對key的自動裝箱(int轉為Integer類型),它內部則是通過兩個數組來進行數據存儲的,一個存儲key,另外一個存儲value,為了優化性能,它內部對數據還採取了壓縮的方式來表示稀疏數組的數據,從而節約內存空間,我們從源碼中可以看到key和value分別是用數組表示。
為什麼是能夠進行二分查找呢?從源碼上看key和value分別是用int類型數組和object數組表示,所以這也是SparseArray的局限性。
private int[] mKeys;
private Object[] mValues;
為什麼說SparseArray比HashMap更省內存,在某些條件下性能更好?
因為SparseArray有以下一個特性,首先它是2個數組,在數據查找的時候無疑會比hashmap快很多,其次在刪除的時候,SparseArray並不會把數組key位置進行刪除,而是把key的索引value置位DELETE標志(這樣就避免了數組delete操作後的array的操作)。當我們下次進行插入的時候,若要插入的位置key的索引value為DELETE標志,則把數據覆蓋給value(只是經歷了set操作,並無其他操作)。否則進行add操作(包含array)。
所以經過以上的情況,我們可以看出,SparseArray相對於HashMap,會越用越快。
缺點
(1)SparseArray僅僅能存儲key為int類型的數據。
(2)插入操作需要復制數組,增刪效率降低 數據量巨大時,復制數組成本巨大,gc()成本也巨大。
(3)數據量巨大時,查詢效率也會明顯下降。
(4)線程不安全問題,類似hashmap
一般我們在滿足下面兩個條件我們可以使用SparseArray代替HashMap:
(1)數據量不大,最好在千級以內
(2)key必須為int類型,這中情況下的HashMap可以用SparseArray代替:
ArrayMap(Android為了優化內存所提供的api)
ArrayMap和SparseArray差不多,不同的是key類型可以是object類型。
ArrayMap的2個數組,一個數組記錄key的hash值,另外一個數組記錄Value值。其他存儲方式和運行思想和SparseArray一致。
線程不安全:hashmap、ArrayMap、SparseArray
❿ 電腦培訓分享數據結構與演算法知識
對於大多數的程序員來說,在學習數據分析等技術的時候需要先了解關於數據結構以及演算法等知識點,下面我們就給大家簡單介紹一下什麼是數據結構?什麼是演算法?
大部分數據結構和演算法教材,在開篇都會給這兩個概念讓悶下一個明確的定義。但是,這些定義都很抽象,對理解這兩個概念並沒有實質性的幫助,反倒會讓你陷入死摳定義的誤區。畢竟,我們現在學習,並不是為了考試,所以,概念背得再牢,不會用也就沒什麼用。
雖然我們說沒必要深挖嚴格的定義,但是這並不等於不需要理解概念。下面我就從廣義和狹義兩個層面,來幫你理解數據結構與演算法這兩個概念。
從廣義上講,數據結構就是指一組數據的存儲結構。演算法就是正滑悔操作數據的一組方法。
圖書館儲藏書籍你肯定見過吧?為了方便查找,圖書管理員一般會將書籍分門別類進行「存儲」。按照一定規律編號,就是書籍這種「數據」的存儲結構。
那我們如何來查找一本書呢?有很多種辦法,你當然可以一本一本地找,也可以先根據書籍類別的編號,是人文,還是科學、計算機,來定位書架,然後再依次查找。籠統地說,這些查找方法都是演算法。
從狹義上講,也就是我們專欄要講的,是指某些著名的數據結構和演算法,比如隊列、棧、堆、二分查找、動態規劃等。這些都是前人智慧的結晶,我們可以直接拿來用。我們要講的這些數據結構和演算法,都是前人從很多實際操作場景中抽象出來的,經過非常多的求證和檢驗,可以高效地幫助我們解決很多實際的開發問題。
那數據結構和演算法有什麼關系呢?為什麼大部分書都把這兩個東西放到一塊兒來講呢?
這是因為,數據結構和演算法是相輔相成的。數據結構是為演算法服務的,演算法要作用在特定的數據結構之上。因此,我們無法孤立數據結構來講演算法,也無法孤立演算法舉正來講數據結構。
比如,因為數組具有隨機訪問的特點,常用的二分查找演算法需要用數組來存儲數據。但如果IT培訓選擇鏈表這種數據結構,二分查找演算法就無法工作了,因為鏈表並不支持隨機訪問。
數據結構是靜態的,它只是組織數據的一種方式。如果不在它的基礎上操作、構建演算法,孤立存在的數據結構就是沒用的。