數據結構,直白的理解,就是研究數據的存儲方式。算法
數據結構是一門學科,它教會咱們「如何存儲具備複雜關係的數據更有助於後期對數據的再利用」。編程
數據結構大體包含如下幾種存儲結構:數組
線性表數據結構
樹結構編程語言
下面對各類數據結構作詳細講解。測試
將具備 「一對一」 關係的數據 「線性」 地存儲到物理空間中,這種存儲結構就稱爲線性存儲結構(簡稱線性表)。this
使用線性表存儲數據的方式能夠這樣理解,即「把全部數據用一根線兒串起來,再存儲到物理空間中」。
使用線性表存儲的數據,要求數據類型必須一致。
線性表並非一種具體的存儲結構,它包含順序存儲結構和鏈式存儲結構,是順序表和鏈表的統稱。spa
上圖中咱們能夠看出,線性表存儲數據可細分爲如下 2
種:設計
3a)
所示,將數據依次存儲在連續的整塊物理空間中,這種存儲結構稱爲順序存儲結構(簡稱順序表);3b)
所示,數據分散的存儲在物理空間中,經過一根線保存着它們之間的邏輯關係,這種存儲結構稱爲鏈式存儲結構(簡稱鏈表);數據結構中,一組數據中的每一個個體被稱爲數據元素,簡稱元素。指針
另外,對於具備 「一對一」 邏輯關係的數據,咱們一直在用「某一元素的左側(前邊)或右側(後邊)」這樣不專業的詞,其實線性表中有更準確的術語:
以下圖,元素 3
它的直接前驅是 2
,此元素的前驅元素有 2
個,分別是 1
和 2
;同理,此元素的直接後繼是 4
,後繼元素也有 2
個,分別是 4
和 5
。
順序表,全名順序存儲結構,是線性表的一種。
順序表存儲數據時,會提早申請一整塊足夠大小的物理空間,而後將數據依次存儲起來,存儲時作到數據元素之間不留一絲縫隙。
順序表,簡單的理解就是經常使用的數組,例如使用順序表存儲 [1, 2, 3, 4, 5]
,如圖:
因爲順序表結構的底層實現藉助的就是數組,所以對於初學者來講,能夠把順序表徹底等價爲數組,但實則不是這樣。數據結構是研究數據存儲方式的一門學科,它囊括的都是各類存儲結構,而數組只是各類編程語言中的基本數據類型,並不屬於數據結構的範疇。
使用順序表存儲數據以前,除了要申請足夠大小的物理空間以外,爲了方便後期使用表中的數據,順序表還須要實時記錄如下 2
項數據:
因爲順序表能夠簡單的理解爲數組,因此在這裏就定義一個數組,將數組看成一個順序表來處理。
和大多數其餘語言不一樣,JavaScript
數組的length
是沒有上界的。 因此存儲容量也是動態變化的。
// 定義一個順序表 function List(list) { this.data = list || []; this.getLength = getLength; this.clear = clear; // 清空順序表 this.insertEl = insertEl; // 插入元素 this.removeEl = removeEl; // 刪除元素 this.changeEl = changeEl; // 更改元素 this.findEl = findEl; // 查找元素 }
向已有順序表中插入數據元素,根據插入位置的不一樣,可分爲如下 3
種狀況:
雖然數據元素插入順序表中的位置有所不一樣,可是都使用的是同一種方式去解決,即:經過遍歷,找到數據元素要插入的位置,而後作以下兩步工做:
例如,在 [1, 2, 3, 4, 5]
的第 3 個位置上插入元素 6
,實現過程以下:
3
個數據元素的位置。
3
以及後續元素 4
和 5
總體向後移動一個位置。
6
放入騰出的位置。
function getLength() { return this.data.length; } /** * @param {any} el 要插入的元素 * @param {Number} index 元素下標 */ function insertEl(el, index) { if (index > this.getLength() || index < 0) { throw new Error('插入位置有錯'); } else { // 插入操做,須要將從插入位置開始的後續元素,逐個後移 for (var len = this.getLength(); len >= index; len--) { this.data[len] = this.data[len - 1]; } // 後移完成後,直接將所需插入元素,添加到順序表的相應位置 this.data[index] = el; return this.data; } }
爲數組添加元素,能夠經過push()
、unshift()
、splice(index, 0, element)
方法實現。
// 向數組頭部添加元素: arr.unshift(); // 向數組末尾添加元素: arr.push(); // 向數組下標 index 處插入元素: arr.splice(index, 0, el)
從順序表中刪除指定元素,只需找到目標元素,並將其後續全部元素總體前移 1
個位置便可。
後續元素總體前移一個位置,會直接將目標元素刪除,可間接實現刪除元素的目的。
例如,從 [1, 2, 3, 4, 5]
中刪除元素 3
的過程,以下圖所示:
js 代碼實現:
/** * @param {Number} index 被刪除元素的下標 */ function removeEl(index) { if (index > this.getLength() || index < 0) { throw new Error('刪除位置有誤'); } else { // 刪除操做 for (var i = index; i < this.getLength() - 1; i++) { this.data[i] = this.data[i + 1]; } this.data.length--; return this.data; } }
刪除數組的元素,能夠經過pop()
、shift()
、splice(index, 1)
方法實現。
// 從數組頭部刪除元素: arr.shift(); // 從數組末尾刪除元素: arr.pop(); // 從數組下標 index 處刪除一個元素: arr.splice(index, 1);
順序表中查找目標元素,可使用多種查找算法實現,好比說二分查找算法、插值查找算法等。
這裏,咱們選擇順序查找算法,具體實現代碼爲:
/** * @param {any} el 須要查找的元素 */ function findEl(el) { for (let i = 0; i < this.getLength(); i++) { if (this.data[i] == el) { return i; // 第一次匹配到的元素的下標 } } return -1; //若是查找失敗,返回 -1 }
查找數組元素的下標,可使用indexOf()
、lastIndexOf()
方法實現。
順序表更改元素的實現過程是:
/** * @param {any} el 須要更改的元素 * @param {any} newEl 爲新的數據元素 */ function changeEl( el, newEl) { const index = this.findEl(el); this.data[index] = newEl; return this.data; }
以上是順序表使用過程當中最經常使用的基本操做,這裏給出完整的實現代碼:
class List { constructor(list) { this.data = list || []; } clear() { delete this.data; this.data = []; return this.data; } getLength() { return this.data.length; } insertEl(index, el) { if (index > this.getLength() || index < 0) { throw new Error('插入位置有錯'); } else { // 插入操做,須要將從插入位置開始的後續元素,逐個後移 for (var len = this.getLength(); len >= index; len--) { this.data[len] = this.data[len - 1]; } // 後移完成後,直接將所需插入元素,添加到順序表的相應位置 this.data[index] = el; return this.data; } } // 經過下標刪除元素 removeEl(index) { if (index > this.getLength() || index < 0) { throw new Error('刪除位置有誤'); } else { // 刪除操做 for (var i = index; i < this.getLength() - 1; i++) { this.data[i] = this.data[i + 1]; } this.data.length--; return this.data; } } changeEl(el, newEl) { const index = this.findEl(el); this.data[index] = newEl; return this.data; } findEl(el) { for (let i = 0; i < this.getLength(); i++) { if (this.data[i] == el) { return i; // 第一次匹配到的元素的下標 } } return -1; //若是查找失敗,返回 -1 } } const list = new List([1, 2, 3, 4, 5]) console.log('初始化順序表:') console.log(list.data.join(',')); console.log('刪除下標爲 0 的元素:') console.log(list.removeEl(0).join(',')); console.log('在下標爲 3 的位置插入元素 6:') console.log(list.insertEl(3, 6).join(',')); console.log('查找元素 3 的下標:') console.log(list.findEl(3)); console.log('將元素 3 改成 6:') console.log(list.changeEl(3, 6).join(','))
程序運行結果:
初始化順序表: 1,2,3,4,5 刪除下標爲 0 的元素: 2,3,4,5 在下標爲 3 的位置插入元素 6: 2,3,4,6,5 查找元素 3 的下標: 1 將元素 3 改成 6: 2,6,4,6,5
數組不老是組織數據的最佳數據結構,緣由以下。在不少編程語言中,數組的長度是固定的,因此當數組已被數據填滿時,再要加入新的元素就會很是困難。在數組中,添加和刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組剛剛進行了添加或刪除操做。
然而,JavaScript
的數組並不存在上述問題,由於使用 split()
方法不須要再訪問數組中的其餘元素了。
JavaScript
中數組的主要問題是,它們被實現成了對象,與其餘語言(好比 C++ 和 Java) 的數組相比,效率很低(請參考 Crockford 那本書的第 6 章)。
若是你發現數組在實際使用時很慢,就能夠考慮使用鏈表
來替代它。除了對數據的隨機訪問,鏈表
幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是更好的選擇。
鏈表,別名鏈式存儲結構
或單鏈表
,用於存儲邏輯關係爲 「一對一」 的數據。
咱們知道,使用順序表(底層實現靠數組)時,須要提早申請必定大小的存儲空間,這塊存儲空間的物理地址是連續的,以下圖所示。
鏈表則徹底不一樣,使用鏈表存儲數據時,是隨用隨申請,所以數據的存儲位置是相互分離的,換句話說,數據的存儲位置是隨機的。
咱們看到,上圖根本沒法體現出各數據之間的邏輯關係。對此,鏈表的解決方案是,每一個數據元素在存儲時都配備一個指針,用於指向本身的直接後繼元素,以下圖所示。
數據元素隨機存儲,並經過指針表示數據之間邏輯關係的存儲結構就是鏈式存儲結構。
鏈表中每一個數據的存儲都由如下兩部分組成:
鏈表中存儲各數據元素的結構以下圖所示:
上圖所示的結構在鏈表中稱爲節點。也就是說,鏈表實際存儲的是一個一個的節點,真正的數據元素包含在這些節點中
:
其實,一個完整的鏈表須要由如下幾部分構成:
頭指針
:一個普通的指針,它的特色是永遠指向鏈表第一個節點的位置。很明顯,頭指針用於指明鏈表的位置,便於後期找到鏈表並使用表中的數據;節點
:鏈表中的節點又細分爲頭節點
、首元節點
和其餘節點
:
頭節點
:其實就是一個不存任何數據的空節點,一般做爲鏈表的第一個節點。對於鏈表來講,頭節點不是必須的,它的做用只是爲了方便解決某些實際問題;首元節點
:因爲頭節點(也就是空節點)的緣故,鏈表中稱第一個存有數據的節點爲首元節點。首元節點只是對鏈表中第一個存有數據節點的一個稱謂,沒有實際意義;其餘節點
:鏈表中其餘的節點;一個存儲 [1, 2, 3]
的完整鏈表結構如圖所示:
鏈表中有頭節點時,頭指針指向頭節點;反之,若鏈表中沒有頭節點,則頭指針指向首元節點。
咱們設計的鏈表包含兩個類:
Node
類用來表示節點;LinkedList
類提供了插入的節點、刪除節點、顯示列表元素的方法,以及其餘輔助方法。Node
類包含兩個屬性,element
和 next
。
// Node 類 class Node { constructor(element) { this.element = element; // element 用來保存節點上的數據 this.next = null; // next 用來保存指向下一節點的指針 } }
LinkedList
類提供了對鏈表進行操做的方法。鏈表只有一個屬性,那就是使用一個 Node
對象來保存該鏈表的頭節點。
function LinkedList() { // head 節點的 next 屬性被初始化爲 null,當有新元素插入時,next 會指向新的元素 this.head = new Node('head'); this.find = find; this.insert = insert; this.findPrevious = findPrevious; this.remove = remove; this.display = display; }
同順序表同樣,向鏈表中增添元素,根據添加位置不一樣,可分爲如下 3
種狀況:
雖然新元素的插入位置不固定,可是鏈表插入元素的思想是固定的,只需作如下兩步操做,便可將新元素插入到指定的位置:
next
指針指向插入位置後的節點;next
指針指向插入節點;例如,咱們在鏈表 [1, 2, 3, 4]
的基礎上分別實如今頭部、中間部位、尾部插入新元素 5
,其實現過程以下圖所示:
注意
:鏈表插入元素的操做必須是先步驟1
,再步驟2
;反之,若先執行步驟2
,除非再添加一個指針,做爲插入位置後續鏈表的頭指針,不然會致使插入位置後的這部分鏈表丟失,沒法再實現步驟1
。
建立一個輔助方法 find()
,該方法遍歷鏈表,查找給定數據。
find()
方法實現:
function find(item) { var currNode = this.head; // 建立一個新節點 while (currNode.element != item) { // 若是當前節點數據不符合咱們要找的節點 currNode = currNode.next; // 當前節點移動到一下節點 } return currNode; }
一旦找到 給定
的節點,就能夠將新節點插入鏈表了。首先,將新節點的 next
指針指向 給定
節點 next
指向的節點。而後設置 給定
節點的 next
屬性指向新節點。
insert()
方法的定義以下:
function insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); // 向鏈表插入節點 newNode newNode.next = current.next; // 將新節點的 next 指針指向插入位置後的節點 (步驟 1) current.next = newNode; // 設置插入位置前的 next 指針指向新節點(步驟2) }
如今已經能夠開始測試咱們的鏈表實現了。然而在測試以前,先來定義一個 display()
方法,該方法用來顯示鏈表中的元素:
function display() { var currNode = this.head; while(!(currNode.next == null)) { // 噹噹前節點的 next 指針爲 null 時 循環結束 //爲了避免顯示頭節點,程序只訪問當前節點的下一個節點中保存的數據 console.log(chrrNode.next.element); currNode = currNode.next; } }
測試程序:
function Node(element) { this.element = element; this.next = null; } function LinkedList() { this.head = new Node('head'); this.find = find; this.insert = insert; this.findPrevious = findPrevious; this.remove = remove; this.display = display; } function find(item) { var currNode= this.head; while(currNode.element != item) { currNode = currNode.next; } return currNode; } function insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; } function findPrevious() {} function remove() {} function display() { var currNode = this.head; while(!(currNode.next == null)) { console.log(currNode.next.element) currNode = currNode.next; } } var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) list.display()
程序運行結果:
1 2 3 4
從鏈表中刪除指定數據元素時,須要進行如下 2
步操做:
直接前驅節點
;next
指針,使其再也不指向待刪除節點;
從鏈表中刪除節點時,須要先找到待刪除節點的直接前驅節點
。找到這個節點後,修改它的 next
指針,咱們能夠定義一個方法 findPrevious()
,來作這件事。
findPrevious()
方法遍歷鏈表中的元素,檢查每個節點的 直接後繼節點
中是否存儲着待刪除數據。若是找到,返回該節點(待刪除節點的直接前驅節點
)。
function findPrevious(item) { var currNode = this.head; while(!(currNode.next == null) && currNode.next.element != item) { currNode = currNode.next; } return currNode; }
刪除節點的原理很簡單,找到該節點的直接前驅節點 prevNode
後,修改它的 next
指針:
prevNode.next = prevNode.next.next;
remove()
方法的 js
實現:
function remove(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } }
測試程序:
... var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) list.display() list.remove(2) list.display()
程序運行結果:
// 沒調用 list.remove(2) 以前 1 2 3 4 // 調用list.remove(2) 1 3 4
更新鏈表中的元素,只需經過遍歷找到存儲此元素的節點,對節點中的數據域作更改操做便可。
function change(index, newElement) { var currNode = this.head; for(var i = 0; i <= index; i++) { currNode = currNode.next; if (currNode == null) { throw new Error('更新位置無效'); return; } } currNode.element = newElement; return currNode; }
最後給出 js
實現的擁有基本操做 增刪改查
的鏈表完整代碼:
class Node { constructor(element) { this.element = element; this.next = null; } } class LinkedList { constructor() { this.head = new Node('head'); } // 鏈表查找元素 find(item) { var currNode= this.head; while(currNode.element != item) { currNode = currNode.next; } return currNode; } // 鏈表添加元素 insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; } // 鏈表查找直接前驅節點(輔助方法) findPrevious(item) { var currNode = this.head; while(!(currNode.next == null) && currNode.next.element != item) { currNode = currNode.next; } return currNode; } // 鏈表刪除節點 remove(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } } // 鏈表更改節點 change(index, newElement) { var currNode = this.head; for(var i = 0; i <= index; i++) { currNode = currNode.next; if (currNode == null) { throw new Error('更新位置無效'); return; } } currNode.element = newElement; return currNode; } display() { var currNode = this.head; while(!(currNode.next == null)) { console.log(currNode.next.element) currNode = currNode.next; } } } var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) console.log('初始化鏈表:') list.display() // 1, 2, 3, 4 list.remove(2) console.log('刪除數據爲 2 的節點: ') list.display() // 1, 3, 4 list.change(2, 6) console.log('將下標爲 2 的 節點數據更改成 6:') list.display() // 1, 3, 6
總體程序運行結果:
初始化鏈表: 1 2 3 4 刪除數據爲 2 的節點: 1 3 4 將下標爲 2 的 節點數據更改成 6: 1 3 6