數據結構是咱們軟件開發中最基礎的部分了,它體現着咱們編程的內功。大多數人在正兒八經學習數據結構的時候估計是在大學計算機課上,而在實際項目開發中,反而感受到用得很少。程序員
其實也不是真的用得少,只不過咱們在使用的時候被不少高級語言和框架組件封裝好了,真正須要本身去實現的地方比較少而已。但別人封裝好了不表明咱們就能夠不關注了,數據結構做爲程序員的內功心法,是很是值得咱們多花時間去研究的,我這就翻開書複習複習:算法
本文就先從你們最常用的「 數組 」和「 鏈表 」聊起。不過在聊數組和鏈表以前,我們先看一下數據的邏輯結構分類。通俗的講,數據的邏輯結構主要分爲兩種:編程
線性的:就是連成一條線的結構,本文要講的數組和鏈表就屬於這一類,另外還有 隊列、棧 等數組
非線性的:顧名思義,數據之間的關係是非線性的,好比 堆、樹、圖 等數據結構
知道了分類,下面咱們來詳細看一下「 數組 」和「 鏈表 」的原理。框架
數組是一個有限的、類型相同的數據的集合,在內存中是一段連續的內存區域。性能
以下圖:學習
數組的下標是從0開始的,上圖數組中有6個元素,對應着下標依次是0、一、二、三、四、5,同時,數組裏面存的數據的類型必須是一致的,好比上圖中存的都是數字類型。數組中的所有元素是「連續」的存儲在一塊內存空間中的,如上圖右邊部分,元素與元素之間是不會有別的存儲隔離的。另外,也是由於數組須要連續的內存空間,因此數組在定義的時候就須要提早指定固定大小,不能改變。spa
數組的訪問:3d
數組在訪問操做方面有着獨特的性能優點,由於數組是支持隨機訪問的,也就是說咱們能夠經過下標隨機訪問數組中任何一個元素,其原理是由於數組元素的存儲是連續的,因此咱們能夠經過數組內存空間的首地址加上元素的偏移量計算出某一個元素的內存地址,以下:
array[n]的地址 = array數組內存空間的首地址 + 每一個元素大小*n
經過上述公式可知:數組中經過下標去訪問數據時並不須要遍歷整個數組,所以數組的訪問時間複雜度是 O(1),固然這裏須要注意,若是不是經過下標去訪問,而是經過內容去查找數組中的元素,則時間複雜度不是O(1),極端的狀況下須要遍歷整個數組的元素,時間複雜度多是O(n),固然經過不一樣的查找算法所需的時間複雜度是不同的。
數組的插入與刪除:
一樣是由於數組元素的連續性要求,因此致使數組在插入和刪除元素的時候效率比較低。
若是要在數組中間插入一個新元素,就必需要將要相鄰的後面的元素所有日後移動一個位置,留出空位給這個新元素。仍是拿上面那圖舉例,若是須要在下標爲2的地方插入一個新元素11,那就須要將原有的二、三、四、5幾個下標的元素依次日後移動一位,新元素再插入下標爲2的位置,最後造成新的數組是:
2三、四、十一、六、1五、五、7
若是新元素是插入在數組的最開頭位置,那整個原始數組都須要向後移動一位,此時的時間複雜度爲最壞狀況即O(n),若是新元素要插入的位置是最末尾,則無需其它元素移動,則此時時間複雜度爲最好狀況即O(1),因此平均而言數組插入的時間複雜度是O(n)
數組的刪除與數組的插入是相似的。
因此總體而言,數組的訪問效率高,插入與刪除效率低。不過想改善數組的插入與刪除效率也是有辦法的,來來來,下面的「 鏈表 」瞭解一下。
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是經過鏈表中的指針連接次序實現的,通常用於插入與刪除較爲頻繁的場景。
上圖是「單鏈表」示例,鏈表並不須要數組那樣的連續空間,它只須要一個個零散的內存空間便可,所以對內存空間的要求也比數組低。
鏈表的每個節點經過「指針」連接起來,每個節點有2部分組成,一部分是數據(上圖中的Data),另外一部分是後繼指針(用來存儲後一個節點的地址),在這條鏈中,最開始的節點稱爲Head,最末尾節點的指針指向NULL。
「 鏈表 」也分爲好幾種,上圖是最簡單的一種,它的每個節點只有一個指針(後繼指針)指向後面一個節點,這個鏈表稱爲:單向鏈表,除此以外還有 雙向鏈表、循環鏈表 等。
雙向鏈表:
雙向鏈表與單向鏈表的區別是前者是2個方向都有指針,後者只有1個方向的指針。雙向鏈表的每個節點都有2個指針,一個指向前節點,一個指向後節點。雙向鏈表在操做的時候比單向鏈表的效率要高不少,可是因爲多一個指針空間,因此佔用內存也會多一點。
循環鏈表:
其實循環鏈表就是一種特殊的單向鏈表,只不過在單向鏈表的基礎上,將尾節點的指針指向了Head節點,使之首尾相連。
鏈表的訪問
鏈表的優點並不在與訪問,由於鏈表沒法經過首地址和下標去計算出某一個節點的地址,因此鏈表中若是要查找某個節點,則須要一個節點一個節點的遍歷,所以鏈表的訪問時間複雜度爲O(n)
鏈表的插入與刪除
也正式由於鏈表內存空間是非連續的,因此它對元素的插入和刪除時,並不須要像數組那樣移動其它元素,只須要修改指針的指向便可。
例如:刪除一個元素E:
例如:插入一個元素:
既然插入與刪除元素只須要改動指針,無需移動數據,那麼鏈表的時間插入刪除的時間複雜度爲O(1)不過這裏指的是找到節點以後純粹的插入或刪除動做所需的時間複雜度。
若是當前還未定位到指定的節點,只是拿到鏈表的Head,這個時候要去刪除此鏈表中某個固定內容的節點,則須要先查找到那個節點,這個查找的動做又是一個遍歷動做了,這個遍歷查找的時間複雜度倒是O(n),二者加起來總的時間複雜度實際上是O(n)的。
其實就算是已經定位到了某個要刪除的節點了,刪除邏輯也不簡單。以「刪除上圖的E節點」爲例,假如當前鏈表指針已經定位到了E節點,刪除的時候,須要將這個E節點的前面一個節點H的後繼指針改成指向A節點,那麼E節點就會自動脫落了,可是當前鏈表指針是定位在E節點上,如何去改變H節點的後續指針呢,對於「單向鏈表」而言,這個時候須要從頭遍歷一遍整個鏈表,找到H節點去修改其後繼指針的內容,因此時間複雜度是O(n),但若是當前是「雙向鏈表」,則不須要遍歷,直接經過前繼指針便可找到H節點,時間複雜度是O(1),這裏就是「雙向鏈表」至關於「單向鏈表」的優點所在。
經過上面的介紹咱們能夠看到「 數組 」和「 鏈表 」各有優點,而且時間複雜度在不一樣的操做狀況下也不相同,不能簡單一句O(1)或O(n)。因此下面咱們找了個經常使用的算法題來練習練習。
算法題:反轉一個單鏈表
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
//定義一個前置節點變量,默認是null,由於對於第一個節點而言沒有前置節點
ListNode pre = null;
//定義一個當前節點變量,首先將頭節點賦值給它
ListNode curr = head;
//遍歷整個鏈表,直到當前指向的節點爲空,也就是最後一個節點了
while(curr != null){
//在循環體裏會去改變當前節點的指針方向,原本當前節點的指針是指向的下一個節點,如今須要改成指向前一個節點,可是若是直接就這麼修改了,那鏈條就斷了,再也找不到後面的節點了,因此首先須要將下一個節點先臨時保存起來,賦值到temp中,以備後續使用
ListNode temp = curr.next;
//開始處理當前節點,將當前節點的指針指向前面一個節點
curr.next = pre;
//將當前節點賦值給變量pre,也就是讓pre移動一步,pre指向了當前節點
pre = curr;
//將以前保存的臨時節點(後面一個節點)賦值給當前節點變量
curr = temp;
//循環體執行鏈表狀態變動狀況:
//NULL<-1 2->3->4->5->NULL
//NULL<-1<-2 3->4->5->NULL
//NULL<-1<-2<-3 4->5->NULL
//NULL<-1<-2<-3<-4 5->NULL
//NULL<-1<-2<-3<-4<-5
//循環體遍歷完以後,pre指向5的節點
}
//完成,時間複雜度爲O(n)
return pre;
}
}
以上,就是對「 數組與鏈表 」的一些思考。
碼字不易啊,喜歡的話不妨轉發朋友吧。😊