鏈表做爲一種數據結構,它存放着有序元素的集合。元素與元素之間經過指針鏈接,所以在鏈表中添加或刪除元素只須要修改指針的指向便可,執行速度相比數組有獲得顯著的提高。node
現實生活中也有許多使用到鏈表的例子,例如兔子舞,每一個人勾肩搭背組合而成,其中人至關於鏈表中的元素,勾肩搭背的手至關於連接每一個人的指針,在隊列中加入一我的,只須要找到想加入的點,斷開鏈接,插入一我的再從新鏈接起來。git
本文將詳解鏈表以及鏈表其餘變相的實現思路並使用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。github
本文主要講解鏈表的代碼實現,對鏈表還不是很瞭解的開發者能夠移步個人另外一篇文章:數據結構:鏈表的基礎知識。web
在實現鏈表以前,咱們先來看看數組與鏈表的區別都有哪些。數組
數組多是最經常使用的一種數據結構,每種語言都實現了數組,元素在內存中是連續存放的,所以數組提供了一個很是方便的[]方法來訪問其元素。數據結構
鏈表存儲有序元素的集合,鏈表中的元素在內存中並不是連續存放,每一個元素由一個存儲元素自己的結點和一個指向下一個元素的指針組成,所以增長或刪除鏈表內的元素只須要改變指針指向便可。編輯器
咱們來總結下鏈表與數組各自的優勢:函數
❝鏈表的優勢:元素經過指針鏈接,改變鏈表內的元素只須要找到元素改變其指針便可,所以數據須要頻繁修改時,使用鏈表做爲數據結構是最優解決方案。 數組的優勢:元素連續存放在內存中,訪問元素能夠直接經過元素下標來訪問,所以數據須要頻繁查詢時,使用數組做爲其數據結構是最優解決方案。post
上面咱們總結了它們的優勢,接下來咱們來看下它們各自的缺點:性能
❝鏈表的缺點:因爲鏈表是經過指針鏈接的,咱們只能直接拿到鏈表頭部的元素,要想訪問其餘元素須要從頭遍歷整個鏈表才能找到咱們想找的元素。所以數據須要頻繁查詢時,使用鏈表將拔苗助長。 數組的缺點:因爲元素是連續存放在內存中的,改變數組內的元素時,須要調整其餘元素的位置。所以數據須要頻繁修改時,使用數組將拔苗助長。
鏈表是由指針將元素鏈接到一塊兒,根據鏈表的特性,咱們能夠知道要實現一個鏈表必須必備如下方法:
通過上述分析後,咱們知道了鏈表的實現思路,接下來咱們就將上述思路轉化爲代碼:
// 助手類: 用於表示鏈表中的第一個以及其餘元素
export class Node<T>{ element: T; next: any; // 默認傳一個元素進來 constructor (element: T) { this.element = element; this.next = undefined; } } 複製代碼
// 默認驗證函數
export function defaultEquals(a: any,b: any) { return a === b; } 複製代碼
// @ts-ignore
import {defaultEquals} from "../../utils/Util.ts"; // @ts-ignore import {Node} from "../../utils/linked-list-models.ts"; 複製代碼
// 定義驗證函數要傳的參數和返回結果
interface equalsFnType<T> { (a: T,b: T) : boolean; } // 聲明鏈表內須要的變量並定義其類型 private count: number; private next: any; private equalsFn: equalsFnType<T>; private head: any; constructor(equalsFn = defaultEquals) { // 初始化鏈表內部變量 this.count = 0; this.next = undefined; this.equalsFn = equalsFn; this.head = null; } 複製代碼
// 鏈表尾部添加元素
push(element: T) { // 聲明結點變量,將元素看成參數傳入生成結點 const node = new Node(element); // 存儲遍歷到的鏈表元素 let current; if(this.head==null){ // 鏈表爲空,直接將鏈表頭部賦值爲結點變量 this.head = node; }else{ // 鏈表不爲空,咱們只能拿到鏈表中第一個元素的引用 current = this.head; // 循環訪問鏈表 while (current.next !=null){ // 賦值遍歷到的元素 current = current.next; } // 此時已經獲得了鏈表的最後一個元素(null),將鏈表的下一個元素賦值爲結點變量。 current.next = node; } // 鏈表長度自增 this.count++; } 複製代碼
removeAt(index: number) {
// 邊界判斷: 參數是否有效 if(index >= 0 && index < this.count){ // 獲取當前鏈表頭部元素 let current = this.head; // 移除第一項 if(index === 0){ this.head = current.next; }else{ // 獲取目標參數上一個結點 let previous = this.getElementAt(index - 1); // 當前結點指向目標結點 current = previous.next; /** * 目標結點元素已找到 * previous.next指向目標結點 * current.next指向undefined * previous.next指向current.next即刪除目標結點的元素 */ previous.next = current.next; } // 鏈表長度自減 this.count--; // 返回當前刪除的目標結點 return current.element } return undefined; } 複製代碼
getElementAt(index: number) {
// 參數校驗 if(index >= 0 && index <= this.count){ // 獲取鏈表頭部元素 let current = this.head; // 從鏈表頭部遍歷至目標結點位置 for (let i = 0; i < index && current!=null; i++){ // 當前結點指向下一個目標結點 current = current.next; } // 返回目標結點數據 return current; } return undefined; } 複製代碼
insert(element: T, index: number) {
// 參數有效性判斷 if(index >= 0 && index <= this.count){ // 聲明結點變量,將當前要插入的元素做爲參數生成結點 const node = new Node(element); // 第一個位置添加元素 if(index === 0){ // 將結點變量(node)的下一個元素指向鏈表的頭部元素 node.next = this.head; // 鏈表頭部元素賦值爲結點變量 this.head = node; }else { // 獲取目標結點的上一個結點 const previous = this.getElementAt(index - 1); // 將結點變量的下一個元素指向目標結點 node.next = previous.next; /** * 此時node中當前結點爲要插入的值 * next爲原位置處的結點 * 所以將當前結點賦值爲node,就完成告終點插入操做 */ previous.next = node; } // 鏈表長度自增 this.count++; return true; } return false; } 複製代碼
indexOf(element: T) {
// 獲取鏈表頂部元素 let current = this.head; // 遍歷鏈表內的元素 for (let i = 0; i < this.count && current!=null; i++){ // 判斷當前鏈表中的結點與目標結點是否相等 if (this.equalsFn(element,current.element)){ // 返回索引 return i; } // 當前結點指向下一個結點 current = current.next; } // 目標元素不存在 return -1; } 複製代碼
remove(element: T) {
// 獲取element的索引,移除索引位置的元素 this.removeAt(this.indexOf(element)) } 複製代碼
// 獲取鏈表長度
size() { return this.count; } // 判斷鏈表是否爲空 isEmpty() { return this.size() === 0; } // 獲取鏈表頭部元素 getHead() { return this.head; } 複製代碼
toString(){
if (this.head == null){ return ""; } let objString = `${this.head.element}`; // 獲取鏈表頂點的下一個結點 let current = this.head.next; // 遍歷鏈表中的全部結點 for (let i = 1; i < this.size() && current!=null; i++){ // 將當前結點的元素拼接到最終要生成的字符串對象中 objString = `${objString}, ${current.element}`; // 當前結點指向鏈表的下一個元素 current = current.next; } return objString; } 複製代碼
完整代碼請移步: LinkedList.ts
鏈表實現後,接下來咱們來測試下鏈表中的每一個函數是否正常工做
const linkedList = new LinkedList();
linkedList.push(12); linkedList.push(13); linkedList.push(14); linkedList.push(15); linkedList.push(16); linkedList.push(17); linkedList.push(18); linkedList.push(19); // 移除索引爲2的元素 linkedList.removeAt(2); // 獲取0號元素 console.log(linkedList.getElementAt(0)); // 查找19在鏈表中的位置 console.log(linkedList.indexOf(19)); // 在2號位置添加22元素 linkedList.insert(22,2); // 獲取鏈表中的全部元素 console.log(linkedList.toString()); 複製代碼
完整代碼請移步:LinkedListTest.js
鏈表有多種不一樣的類型,雙向鏈表就是其中一種,接下來咱們來說解雙向鏈表的實現。
實現以前咱們先來看看雙向鏈表與普通鏈表的區別:
說完他們的區別後,咱們來看看雙向鏈表的優勢:雙向鏈表相比普通鏈表多了一個指針,這個指針指向鏈表中元素的上一個元素,所以咱們能夠從鏈表的尾部開始遍歷元素對鏈表進行操做,假設咱們要刪除鏈表中的某個元素,這個元素的位置靠近鏈表的末尾,咱們就能夠從鏈表的末尾來找這個元素,而鏈表只能從其頭部開始找這個元素,此時雙向鏈表的性能相比鏈表會有很大的提高,由於它須要遍歷的元素少,時間複雜度低。
咱們拿雙向鏈表和鏈表進行比對後發現,雙向鏈表是在鏈表的基礎上加多了一個指針(prev)的維護,所以咱們能夠繼承鏈表,重寫與鏈表不一樣的相關函數。
雙向鏈表須要重寫的函數有:尾部插入元素(push)、任意位置插入元素(insert)、任意位置移除元素(removeAt)。
接下來咱們來捋一下,上述須要重寫函數的實現思路:
咱們已經捋清了實現思路,接下來咱們將上述實現思路轉換爲代碼:
實現雙向鏈表以前,咱們須要對鏈表的輔助類進行修改。
export class DoublyNode<T> extends Node<T>{
prev: any; constructor(element: T, next?: any, prev?: any) { // 調用Node類的構造函數 super(element,next); // 新增prev屬性,指向鏈表元素的上一個元素 this.prev = prev; } } 複製代碼
export default class DoublyLinkedList extends LinkedList{
} 複製代碼
private tail: any;
constructor(equalsFn = defaultEquals) { // 調用Node類的構造函數 super(equalsFn); // 新增屬性,用於指向鏈表的最後一個元素 this.tail = undefined; } 複製代碼
push(element: T) {
// 建立雙向鏈表輔助結點 const node = new DoublyNode(element); if (this.head == null){ // 鏈表頭部爲空,頭部和尾部都指向node this.head = node; this.tail = node; }else{ // 將鏈表尾部結點中的next指向node this.tail.next = node; // 將node結點中的prev指向當前鏈表尾部元素 node.prev = this.tail; // 當前鏈表末尾元素指向node this.tail = node; } // 鏈表長度自增 this.count++; } 複製代碼
insert(element: T, index: number) {
// 參數有效性判斷 if(index >=0 && index <= this.count){ // 建立結點 const node = new DoublyNode(element); // 聲明鏈表元素輔助變量(current),默認指向當前鏈表頭部(this.head) let current = this.head; // 鏈表頭部添加元素 if(index === 0){ // 鏈表頭部爲空 if(this.head == null){ // 調用push方法 this.push(element); }else{ // 不爲空,將node.next指向當前頭部元素 node.next = this.head; // 鏈表頭部的元素結點中上一個位置指向node current.prev = node; // 頭部元素指向node this.head = node; } }else if(index === this.count){ // 鏈表尾部添加元素,鏈表元素輔助變量指向擋臉鏈表尾部元素 current = this.tail; // 鏈表元素輔助變量結點中的下一個元素指向node current.next = node; // node結點中的prev指向current node.prev = current; // 當前鏈表尾部元素指向node this.tail = node; }else{ // 鏈表的其餘位置插入元素 const previous = super.getElementAt(index - 1); // 元素變量指向目標結點 current = previous.next; // node的下一個指向目標結點位置的元素 node.next = current; // 目標結點指向結點變量 previous.next = node; // 目標結點的上一個結點指向結點變量 current.prev = node; // 結點插入完畢,調整結點的上一個指針指向 node.prev = previous; } // 鏈表長度自增 this.count++; // 返回true return true; } return false } 複製代碼
removeAt(index: number): any {
// 參數有效性判斷 if(index >=0 && index < this.count){ // current變量指向鏈表頭部 let current = this.head; if(index === 0){ this.head = current.next; if(this.count === 1){ // 鏈表長度爲1,直接將鏈表的末尾元素指向設爲undefined this.tail = undefined; }else{ // 將鏈表頭部的上一個元素指向undefined this.head.prev = undefined; } }else if(index === this.count - 1){ // 鏈表末尾移除元素 current = this.tail; // 鏈表末尾元素指向其上一個元素 this.tail = current.prev; // 鏈表末尾的下一個元素設爲undefined this.tail.next = undefined; }else{ // 雙向鏈表其餘位置移除元素 current = super.getElementAt(index); // 獲取當前要移除元素的上一個元素 const previous = current.prev; // 目標元素的下一個元素指向當前要移除元素的下一個元素 previous.next = current.next; // 當前要移除元素的下一個元素指向要移除元素的上一個元素 current.next.prev= previous; } // 鏈表長度自減 this.count--; // 返回當前要移除的元素 return current.element; } return undefined; } 複製代碼
getTail(){
return this.tail; } 複製代碼
clear() {
super.clear(); this.tail = undefined; } 複製代碼
inverseToString() {
if(this.tail == null){ return ""; } let objString = `${this.tail.element}`; // 獲取鏈表尾部元素的上一個元素 let previous = this.tail.prev; while (previous!=null){ // 將當前獲取到的鏈表元素拼接至鏈表字符串對象中 objString = `${objString}, ${previous.element}`; // 獲取當前鏈表尾部元素的上一個元素 previous = previous.prev; } return objString; } 複製代碼
完整代碼請移步:DoublyLinkedList.ts
雙向鏈表實現後,咱們測試下雙線鏈表中的函數是否都正常工做。
const doublyLinkedList = new DoublyLinkedList();
// 雙向鏈表尾部插入元素 doublyLinkedList.push(12); doublyLinkedList.push(14); doublyLinkedList.push(16); // 雙向鏈表任意位置插入元素 doublyLinkedList.insert(13,1); doublyLinkedList.insert(11,0); doublyLinkedList.insert(14,4); //移除指定位置元素 doublyLinkedList.removeAt(4); doublyLinkedList.insert(15,4); // 刪除鏈表中的元素 doublyLinkedList.remove(16); console.log(doublyLinkedList.toString()); // 獲取鏈表尾部元素 console.log(doublyLinkedList.getTail()); console.log(doublyLinkedList.inverseToString()); // 獲取鏈表長度 console.log("鏈表長度",doublyLinkedList.size()) doublyLinkedList.removeAt(4); // 清空鏈表 doublyLinkedList.clear(); console.log(doublyLinkedList.isEmpty()); 複製代碼
完整代碼請移步:DoublyLinkedListTest.js
循環鏈表也屬於鏈表的一種變體,它與鏈表的惟一區別在於,最後一個元素指向鏈表頭部元素,並不是undefined。
循環鏈表相對於鏈表,改動地方較少,在首、尾插入或刪除元素時,須要更改其指針指向,所以咱們只須要繼承鏈表,而後重寫插入和移除方法便可。
重寫插入方法(insert)
重寫移除方法(removeAt)
咱們捋清思路後,將上述思路轉化爲代碼
export default class CircularLinkedList<T> extends LinkedList<T>{
} 複製代碼
constructor(equalsFn = defaultEquals) {
super(equalsFn); } 複製代碼
insert(element: T, index: number) {
if(index >= 0 && index <= this.count){ // 聲明結點變量 const node = new Node(element); // 聲明鏈表元素變量,默認指向當前鏈表頭部元素 let current = this.head; if(index === 0){ // 鏈表頭部添加元素 if(this.head == null){ // 鏈表頭部爲空 this.head = node; // 鏈表的最後一個結點指向鏈表頭部 node.next = this.head; }else{ // 鏈表頭部不爲空,node中的next指向當前鏈表頭部 node.next = current; // 確保最後一個元素指向新添加的元素,current指向當前元素的最後一個元素 current = this.getElementAt(this.size()); // 更新最後一個元素 this.head = node; current.next = this.head; } }else{ // 鏈表其餘位置插入元素 const previous = this.getElementAt(index - 1); node.next = previous.next; previous.next = node; } this.count++; return true; } return false; } 複製代碼
removeAt(index: number): any {
if(index >= 0 && index < this.count){ let current = this.head; if (index === 0){ if(this.size() === 1){ //鏈表長度爲1直接將鏈表頭部指向undefined this.head = undefined; }else{ // 鏈表長度不爲1,保存鏈表頭部元素,將其從循環鏈表中移除 const removed = this.head; // 鏈表元素變量指向鏈表尾部 current = this.getElementAt(this.size() - 1); // 鏈表頭部指向鏈表頭部元素中的next this.head = this.head.next; // 鏈表尾部元素中的next指向新的鏈表頭部 current.next = this.head; // 更新鏈表元素的引用,用於返回當前移除的值 current = removed; } }else{ const previous = this.getElementAt(index - 1); current = previous.next; previous.next = current.next; } this.count--; return current.element; } return undefined; } 複製代碼
完整代碼請移步: CircularLinkedList.ts
循環鏈表實現後,咱們來測試下上述代碼是否正常運行
const circularLinkedList = new CircularLinkedList();
circularLinkedList.push(11); circularLinkedList.push(12); circularLinkedList.push(13); // 循環鏈表的0號位置插入元素 circularLinkedList.insert(10,0); console.log(circularLinkedList.toString()); // 獲取鏈表的最後一個元素 console.log(circularLinkedList.getElementAt(3)) 複製代碼
完整代碼請移步: CircularLinkedListTest.js
有序鏈表也屬於鏈表的一種變相實現,它不一樣於鏈表的是,插入鏈表的元素會經過一個元素比對函數,對要插入的元素和鏈表內的元素進行比較,將要插入的元素放到鏈表合適的位置。
由於有序鏈表屬於鏈表的一種變相,因此咱們能夠繼承鏈表,只須要重寫鏈表的插入函數實現獲取插入元素正確位置函數便可。
咱們有了實現思路,接下來咱們將上述思路轉化爲代碼:
export default class OrderedList<T> extends LinkedList<T>{
} 複製代碼
const Compare = {
LESS_THAN: -1, BIGGER_THAN: 1 } // 比較兩個元素大小,若是a < b則返回-1,不然返回1 function defaultCompare(a: any, b: any) { if(a === b){ return 0; } return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN; } 複製代碼
constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
super(equalsFn); this.compareFn = compareFn; } 複製代碼
getIndexNextSortedElement(element: T) {
let current = this.head; let i = 0; // 遍歷整個鏈表,直至找到須要插入元素的位置 for (; i < this.size() && current; i++) { // 用compareFn函數比較傳入構造函數的元素 const comp = this.compareFn(element, current.element); // 要插入小於current的元素時,咱們就找到了插入元素的位置 if (comp === Compare.LESS_THAN) { return i; } // 繼續下一輪遍歷 current = current.next; } // 迭代完全部的元素沒有找到符合條件的,則返回鏈表的最後一個元素位置 return i; } 複製代碼
insert(element: T, index: number = 0): boolean {
if(this.isEmpty()){ // 鏈表爲空直接調用父級的insert方法往0號元素插入元素 return super.insert(element, 0); } // 鏈表不爲空,獲取插入元素的正確位置 const pos = this.getIndexNextSortedElement(element); // 獲得位置後調用父級的插入方法往正確位置插入元素 return super.insert(element,pos); } 複製代碼
完整代碼請移步: OrderedList.ts
咱們來測試下上面寫的有序鏈表內的函數是否都正常工做
const orderedList = new OrderedList();
orderedList.insert(12); orderedList.insert(11); orderedList.insert(18); orderedList.insert(1); console.log(orderedList.toString()); 複製代碼
完整代碼請移步:OrderedListTest.js