數組是咱們很是熟悉且經常使用的一種數據結構。但咱們發現,數組不老是組織數據的最佳數據結構。由於在不少編程語言中,數組的長度是固定的,因此當數組已經被數據填滿時,再加入新的元素就會很是困難。同時,在數組中添加或刪除元素也很麻煩,由於須要將數組中的其餘元素向前或向後平移,以反映數組進行了添加或刪除的操做。
雖說在JavaScript中的數組不存在上述問題,咱們使用splice()
方法不須要再訪問數組中的其餘元素。可是在JavaScript中,數組被實現成了對象,所以與其餘語言中的數組相比,效率很低。
在不少狀況下,當咱們發現數組在實際使用時很慢,就能夠考慮使用鏈表來代替它。除了對數據的隨機訪問,鏈表幾乎能夠用在任何可使用一維數組的狀況中。若是須要隨機訪問,數組仍然是最好的選擇。算法
鏈表是由一組節點組成的集合。每一個節點都使用一個對象的引用指向它的後繼。指向另外一個節點的引用叫作鏈。以下圖所示
數組元素依靠它們的位置進行引用,而鏈表元素依靠相互關係進行引用。如咱們能夠說Item2在Item1後面,而不能說Item2是鏈表中的第二個元素。
咱們所說的遍歷鏈表,就是跟着連接,從鏈表的首元素一直走到尾元素(不包括鏈表的頭節點)。
咱們能夠發現,鏈表的尾元素指向一個null節點。編程
要標識出鏈表的起始節點有些麻煩,所以咱們常常會在鏈表最前面有一個特殊節點,叫作頭節點。數組
鏈表中插入一個節點的效率很高,咱們只須要修改其前面的節點,使其指向新加入的節點,同時將新加入的節點指向原來前驅指向的節點便可。數據結構
鏈表中刪除一個節點也很是容易。將待刪元素的前驅節點指向待刪元素的後繼節點,再講待刪元素指向null便可。編程語言
咱們將用JavaScript構造一個基於對象的鏈表結構,各部分功能使用註釋說明。函數
/** * Node類 表示節點,咱們使用構造函數來建立節點 * element 用來保存節點上的數據 * next 用來保存指向下一個節點的連接 * @param {*} element */ function Node (element) { this.element = element this.next = null } /** * LList類 提供對鏈表操做的方法 * find 用於查找元素 * insert 用於插入新節點 * display 用於遍歷顯示鏈表結構 * findPrev 用於遍歷查找待刪除數據的前一個節點 * remove 用於刪除節點 */ function LList () { this.head = new Node('head') this.find = find this.insert = insert this.display = display this.findPrev = findPrev this.remove = remove } /** * find() 方法用於經過遍歷鏈表,查找給定數據 * 返回保存該數據的節點 * @param {*} item */ function find (item) { // 初始化當前位置爲鏈表頭部 let currNode = this.head // 循環遍歷尋找當前位置並返回 while ((currNode != null) && (currNode.element != null) && (currNode.next != null)) { currNode = currNode.next } return currNode } /** * insert() 方法用於插入新節點 * @param {*} newEle * @param {*} item */ function insert (newEle, item) { // 建立新節點 let newNode = new Node(newEle) // 查找要插入的節點位置 let current = this.find(item) // 將新節點的後繼指向要插入位置的後繼 if (current != null) { newNode.next = current.next // 將要插入位置的後繼指向新節點 current.next = newNode } else { // current 爲null時 newNode.next = null this.head.next = newNode } } /** * findPrev() 方法用於遍歷查找待刪除數據的前一個節點 * @param {*} item */ function findPrev (item) { // 初始化當前節點爲頭節點 let currNode = this.head // 當前節點的後繼爲item時中止遍歷並返回,即返回待查找節點的前驅節點 while (!(currNode.next == null) && (currNode.next.element != item)) { currNode = currNode.next } return currNode } /** * remove() 方法用於刪除一個節點 * @param {*} item */ function remove (item) { // 找到item數據節點的前驅節點 let prevNode = this.findPrev(item) if (!(prevNode.next == null)) { // 將前驅節點的後繼節點賦值爲其後繼節點的後繼節點,即跳過了待刪節點 prevNode.next = prevNode.next.next } } /** * display() 方法用於遍歷鏈表 */ function display () { // 初始化當前節點爲頭節點 let currNode = this.head while (!(currNode.next == null)) { // 遍歷輸出節點,並指向下一節點 console.log(currNode.next.element) currNode = currNode.next } }
// 測試代碼 let students = new LList() students.insert('Miyang', 'head') students.insert('Tom', 'Miyang') students.insert('Jerry', 'Tom') students.remove('Tom') students.display() // 輸出結果 Miyang Tom Jerry
關於雙向鏈表的實現,咱們只須要在單向鏈表的基礎上,增長一個指向前驅節點的連接。
實現代碼以下:測試
/** * Node類 表示節點,咱們使用構造函數來建立節點 * element 用來保存節點上的數據 * next 用來保存指向下一個節點的連接 * @param {*} element */ function Node (element) { this.element = element this.next = null this.previous = null } /** * LList類 提供對鏈表操做的方法 * find 用於查找元素 * insert 用於插入新節點 * display 用於遍歷顯示鏈表結構 * findPrev 用於遍歷查找待刪除數據的前一個節點 * remove 用於刪除節點 */ function LList () { this.head = new Node('head') this.find = find this.insert = insert this.display = display this.findPrev = findPrev this.remove = remove this.findLast = findLast this.dispReverse = dispReverse } /** * find() 方法用於經過遍歷鏈表,查找給定數據 * 返回保存該數據的節點 * @param {*} item */ function find (item) { // 初始化當前位置爲鏈表頭部 let currNode = this.head // 循環遍歷尋找當前位置並返回 while ((currNode != null) && (currNode.element != null) && (currNode.next != null)) { currNode = currNode.next } return currNode } /** * insert() 方法用於插入新節點 * @param {*} newEle * @param {*} item */ function insert (newEle, item) { // 建立新節點 let newNode = new Node(newEle) // 查找要插入的節點位置 let current = this.find(item) // 將新節點的後繼指向要插入位置的後繼 if (current != null) { newNode.next = current.next newNode.previous = current // 將要插入位置的後繼指向新節點 current.next = newNode } else { // current 爲null時 newNode.next = null newNode.previous = null this.head.next = newNode } } /** * findPrev() 方法用於遍歷查找待刪除數據的前一個節點 * @param {*} item */ function findPrev (item) { // 初始化當前節點爲頭節點 let currNode = this.head // 當前節點的後繼爲item時中止遍歷並返回,即返回待查找節點的前驅節點 while (!(currNode.next == null) && (currNode.next.element != item)) { currNode = currNode.next } return currNode } /** * remove() 方法用於刪除一個節點 * @param {*} item */ function remove (item) { // 找到item數據節點的前驅節點 let currNode = this.find(item) if (!(currNode.next == null)) { currNode.previous.next = currNode.next currNode.next.previous = currNode.previous currNode.next = null currNode.previous = null } } /** * display() 方法用於遍歷鏈表 */ function display () { // 初始化當前節點爲頭節點 let currNode = this.head while (!(currNode.next == null)) { // 遍歷輸出節點,並指向下一節點 console.log(currNode.next.element) currNode = currNode.next } } /** * findLast() 方法用於找到鏈表中最後一個節點 */ function findLast () { let currNode = this.head while (!(currNode.next == null)) { currNode = currNode.next } return currNode } /** * dispReverse() 方法用於反向遍歷鏈表 */ function dispReverse () { let currNode = this.head currNode = this.findLast() while (!(currNode.previous == null)) { console.log(currNode.element) currNode = currNode.previous } }
// 測試代碼 let students = new LList() students.insert('Miyang', 'head') students.insert('Tom', 'Miyang') students.insert('Jerry', 'Tom') students.remove('Tom') students.display() console.log() students.dispReverse() // 輸出結果 Miyang Tom Jerry Jerry Tom Miyang
循環鏈表和單向鏈表類似,惟一的區別是,在建立循環鏈表時,讓其頭節點的next屬性指向它自己,即:this
head.next = head
修改LList類的構造函數:spa
function LList () { this.head = new Node('head') this.head.next = this.head this.find = find this.insert = insert this.display = display this.findPrev = findPrev this.remove = remove this.findLast = findLast this.dispReverse = dispReverse }
同時,其餘地方也須要修改,如display()
方法,不然會形成死循環code
function display () { // 初始化當前節點爲頭節點 let currNode = this.head while (!(currNode.next == null) && !(currNode.next.element == 'head')) { // 遍歷輸出節點,並指向下一節點 console.log(currNode.next.element) currNode = currNode.next } }
一樣的,其餘方法也須要作相似修改,在此就不一一舉例了。
上面對JavaScript實現鏈表作了基本介紹,你們也能夠嘗試去定義一些其餘方法,如在鏈表中向前移動n個節點advance(n)
、在雙向鏈表中向後移動n個節點back(n)
等。
參考資料:數據結構與算法JavaScript描述 第6章 鏈表 因爲書上的源代碼出現了錯誤,所以代碼根據實際運行結果作了相應修改。