陣列與連結串列
陣列與連結串列對於每一門程式語言來說都是重要的資料結構之一。陣列是一段記憶體連續的,有序的元素序列,大小固定,初始化時需要指定可承載的元素個數; 連結串列是非連續、非順序的儲存結構, 資料元素的順序是通過指標連結實現的。
開發中選擇陣列還是連結串列,這就要結合場景和它們各自優缺點.
陣列的優缺點
陣列的記憶體是連續, 連續有什麼意義,它記憶體地址是連續的, 比如建立一個new Object[5], 假如第一個記憶體地址是init_address, 第二個地址就是init_address + 1。所有陣列隨機訪問很快,訪問第某個元素,用初始的地址加上下標就能找到啦, 陣列遍歷也很快。 陣列的大小固定, 陣列在元素刪除和新增就帶來一點麻煩,在實際開發中通常使用陣列容器,比如java的ArrayList,ArrayList有個預設陣列容量, 為10。 ArrayList.add方法中當新增第11個元素時,需要擴容建立一個新的陣列,還需要把原陣列的元素複製到新的陣列。假如 ArrayList當前有8個元素,當移除第5個元素時,並不是簡單把第5個元素設定為null, 還要把第6 - 8 個元素都往前挪移1位。當ArrayList的元素是int等基本型別,會涉及到裝箱和拆箱,相對與直接使用Int陣列效能會稍微差一點,但是有時方便,犧牲一點效能也可以。
連結串列的優缺點
連結串列的元素通過指標連結, 故大小是無限,只要不爆記憶體,不需要像陣列那樣擴容。連結串列的新增和刪除就相對簡單點,下面以單連結串列為例,新增一個元素,把上一個元素的next指標指向一個新的元素即可; 假如該連結串列有3個元素,當刪除第2個元素時,把第一個元素的next指標指向第3個元素。連結串列元素遍歷相對陣列慢一點,而且隨機訪問第某個元素也相對麻煩,需要遍歷。
連結串列根據結構通常可以分為:單連結串列、雙向連結串列、迴圈連結串列。
單連結串列:
雙向連結串列:
迴圈連結串列:
單連結串列中有環:
上面圖中有個單連結串列中有環,需要注意一下,在使用單連結串列時,在遍歷過程, 以最後元素的next指標為null作為結束的標誌,但是如果出現如圖的情況,就會出現死迴圈。對於連結串列的程式碼實現,不多說,可以看java中LinkedList類, LinkedList實現了雙向連結串列。
棧與佇列
棧是一種操作受限的線性表, 僅允許在一端進行插入和刪除運算, 後進者先出,先進者後出。舉個形象點的例子,有個硬幣大的杯子,往杯子裡一個個地放硬幣, 所有取硬幣只能從頂部一個個取出。(不能倒,杯子用502黏住啦) 這裡的硬幣就是程式裡的元素,而這個杯子就是"棧"。
怎麼實現一個棧?
上面提到陣列和連結串列都可以實現。以陣列實現為例,簡單描述一下,壓棧(意思是放一個元素)過程只要按順序依次放到陣列的index裡,從index=0開始; 陣列可以訪問任意index的元素,而出棧(意思是取一個元素)只能從頂部出, 所以需要限制陣列裡元素的訪問,取出時只能從裝有元素中最大index中取出即可。當然壓棧時陣列也可能需要擴容,一些細節就不多說啦, java中Stack類就是一個實現棧的例子。
佇列也是一種操作受限的線性表, 只允許在表的一端進行刪除操作,而在另一端進行插入操作,先進者先出。來個形象的例子,有個一根玻璃球大的水管, 傾斜著放(假設左邊高右邊低),在左邊不停地放入球,球就會從右邊出, 而這水管就可以簡單理解為"佇列"。佇列也可以使用陣列或者連結串列實現。
棧與佇列應用場景
棧的應用場景很廣泛,下面舉一些例子:
-
瀏覽器的前進和後退可以用棧來實現, 一個標籤頁是一個棧,在開啟一個網壓棧,在網頁中點一個連結,也就是前進,再壓棧,後退對應就是出棧。 瀏覽器有新建一個標籤或者在新的標籤裡開啟頁面,就是建立一個新的棧,並把開啟連結壓棧。
-
安卓應用的介面前進後退也是,有Activity棧,Fragment棧。
-
函式呼叫也可以用棧實現,在進入被呼叫函式的時候,分配一段棧空間給這個函式的變數,在函式執行結束的時候,將棧頂復位,正好回到呼叫函式的作用域內。
-
表示式求值中應用,比如 4 + (6 - 12 + 2* 2) * 2計算。
表示式運算過如下圖所示:
佇列通常存在不同的型別,比如迴圈佇列,併發佇列,阻塞佇列等, 不同的佇列使用場景有所不同。
- 阻塞佇列, 當佇列為空的時候,從隊頭取資料會被阻塞, 直到佇列中有了資料才能返回;如果佇列已經滿了,那麼插入資料的操作也會被阻塞,直到佇列中有空閒位置後再插入資料,然後再返回。 阻塞佇列通常用在生產者消費者模型中,生產資料和消費資料的速率不一致,如果生產資料速度快一些,消費者處理不過來,就可能會導致資料丟失。這時候我們就可以用阻塞佇列來解決。
- 佇列應用線上程池請求排隊的場景, 當執行緒池沒有空閒執行緒時,新的任務請求執行緒資源,執行緒池通常會使用佇列來儲存排隊的請求,為什麼用佇列, 因為佇列先進先出,可以比較公平處理排隊的請求。如果是使用連結串列的實現方式,可以實現無限排隊的無界佇列, 但是可能會導致過多請求排隊,造成處理響應時間慢。如果使用陣列的實現方式,是有界佇列, 當排隊請求滿了,接下來的請求會被拒絕。所有這兩種策略可以看場景使用。