前端基礎算法總結- 鏈表

鏈表

鏈表在react的更新中有所使用,在面試中也是比較常見;比較基礎的鏈表格式有兩種:單鏈表環形鏈表node

前置

建立鏈表

class LinkNode {
    constructor(value){
      this.value = value;
      this.next = null;
    }
 }
複製代碼

建立整個鏈表

function createLink(){
    this.head = null;
 }
 createLink.prototype.add=function(value){
   let linknode = new LinkNode(value)
   if(!this.head){
     this.head = linknode;
   }else{
     //進行遍歷當前鏈表 添加到最後面
     this.insertNode(linknode)
   }
 }
 createLink.prototype.insertNode=function(node){
   let curr = this.head;
   while(curr){ 
     if(!curr.next){
       curr.next = node;
       return ;
     }
     curr = curr.next;
   } 
 }

複製代碼

單鏈表

獲取鏈表長度

function getLinkLength(link){
   if(!link) return 0
   let number = 1;
   let curr = link;
   while(curr){
     number++;
     curr = curr.next;
   }
   return  number;
 }
複製代碼

輸出單鏈表的倒數第k個節點

  • 輸出最後一個節點
  • 從前日後遍歷鏈表,若是遍歷的+第k==鏈表長度則是當前節點

從頭至尾開始查找react

/** * 獲取倒數第k個元素 * @param {*} link * @param {*} k */
function getKlineNode(link,k){
  //獲取當前link長度
  let linkSize = getLinkLength(link);
  if(linkSize <k) return null;
  let curr = link,nu =1;
  while(curr){
    if(k+nu == linkSize){
      return curr.value;
    }
    nu++;
    curr = curr.next;
  }
  return null;
}
複製代碼

遞歸方法

  • 定義num = k
  • 使用遞歸方式遍歷至鏈表末尾。
  • 由末尾開始返回,每返回一次 num 減 1
  • 當 num 爲 0 時,便可找到目標節點
let num = 0
function dgGetKlineNode(link,k){
  num = k;
  if(!link) return null;
  let curr = dgGetKlineNode(link.next,k)
  //當前存在值 返回繼續遍歷
  if(curr){
     return curr;
  }else{
    //不存在值 說明當前一次遍歷結束
    num --;//遞歸一次 減去0
    if(num ==0){//num爲0 說明到了節點
       return link.value
    }
  }
  return null
}
複製代碼

image.png

圖1 找節點倒數第k個節點

雙指針法

function dobuleGetKeyLink(link,key){
  if(!link) return null
  let curr = link,curr2 =link;
  //curr2先走k步
  for(var i =0;i<key;i++){
    curr2 = curr2.next;
  }
  //curr2到頭,此時curr的位置正好是倒數第k的位置
  while(curr2){
    curr2 = curr2.next;
    curr = curr.next;
  }
  return curr.value 
}

複製代碼

* 從尾到頭打印鏈表

藉助三方空間存儲當前已經遍歷過的樹形結構面試

非遞歸

function printReverseLink(link){
  //藉助stack存儲已經遍歷的結構
  let stack = [];
  let curr = link;
  while(curr){
    stack.unshift(curr.value)
    curr=curr.next;
  }
  return stack.join("->")
}
let result = printReverseLink(link.head)
console.log(result)
複製代碼

遞歸

let stackdB = []
function dbPrintReverseLink(link){
  //當前節點不爲空
    if(link){
      //繼續遍歷下一個元素
      if(link.next!=null){
        dbPrintReverseLink(link.next)
      }
      //而後在進行存儲當前節點,保證後面的節點都在當前節點的前面
      stackdB.push(link.value)
    }
}
dbPrintReverseLink(link.head);
console.log(stackdB.join("->"))
複製代碼

* 反轉鏈表

鏈表的反轉,即 先後置換,頭結點標稱尾部markdown

  • 鏈表遍歷
  • 定義前pre節點
  • 定義後node節點,向後移動的時候進行指針的指向調轉
function reverseLink(link){
  if(!link) return null; 
  let pre = null,pReversedHead;
  let curr = link; 
  while(curr){
    let needNext = curr.next;//建立一個臨時的next 
    //調換當前的pre 和curr的指向 
    curr.next = pre;
    pre = curr;  
    curr = needNext;   
  }    
  return pre 
}
複製代碼

* 刪除鏈表中節點、要求時間複雜度o(1)

  • 遍歷鏈表 若是存在key == linkNode.value則進行刪除
  • 注意邊界值的刪除,多是最後一個元素
function deleteSingleNode(link,node){
  if(!link) return null;
  let curr = link,pre=null;
  while(curr){
     //當前存在 則刪除
    if(curr.value == node){ 
      //不是刪除的在最後一個
      if(curr.next){
        curr.value = curr.next.value;
        curr.next =  curr.next.next;
        break;
      }else{
        //最後一個元素
        pre.next = null;
        break;
      }  
    } 
    pre = curr;
    curr  = curr.next; 
  }
  //清除掉空的元素
}
複製代碼

刪除鏈表中重複的節點

是否保留一個節點 給定一個有序鏈表,如有一個以上相同節點,則將該元素值的節點均刪除。函數

  • 給定條件爲有序 這樣保證了咱們不須要重複去遍歷了;
  • 若是鏈表是是無序的,則不須要進行刪除;

思路-:遍歷鏈表,記錄元素,若是存在相同的則進行所有刪除

function deleteMoreNode(link){
    if(!link) return null
    let curr = link,pre = link;
    //第一個特殊
    if(curr.next && curr.value == curr.next.value){
      var isFirst = true;
    }
    while(curr){
      //比較當前和後面一個是否相等
      if(curr.next){
        //兩個元素相等
        if(curr.value == curr.next.value){
          //持續遍歷 直到不相等
          while(curr.next && curr.value == curr.next.value ){
            curr = curr.next;
          }  
          curr = curr.next;  
          pre.next = curr;   
        }else{ 
          pre = curr; 
          curr = curr.next;  
        }   
      } else{
        curr=curr.next;
      }
    }
    //第一元素相等 刪除第一個元素 
    if(isFirst){  
      if(link.next){
        link.value = link.next.value;
        link.next = link.next.next || null; 
      }else{ 
        link.value = null
      }    
    }  
 }
複製代碼

其餘思路:

遍歷鏈表,保留在棧中,每次進行比較棧中是否需存在該元素,存在則進行刪除,不存在則繼續oop

function deleteMoreNodeStack(link){
  if(!link) return null;
  let stack =[];
  stack.push(link.value)
  let curr = link.next,pre = link; 
  while(curr){ 
    if(stack.indexOf(curr.value)>-1){   
      //存在當前元素 進行刪除 
      while(stack.indexOf(curr.value)>-1){  
         curr = curr.next;
         //此時說明元素已經不存在了
         if(!curr){
           pre.value = null;
           pre.next = null
           return ;
         }
      }   
      if(curr){
        stack.push(curr.value)
        pre.value = curr.value;
        pre.next = curr.next;
      }     
    }else{ 
      stack.push(curr.value)
      pre = curr;   
    }
    curr &&  (curr = curr.next) 
  } 
   
}
複製代碼

* 兩個鏈表中的第一個公共節點

思路:ui

單鏈表的指針是指向下一個節點的,若是兩個單鏈表的第一個公共節點就說明他們後面的節點都是在一塊兒的。相似下圖,因爲兩個鏈表的長度多是不一致的,因此首先比較兩個鏈表的長度m,n,而後用兩個指針分別指向兩個鏈表的頭節點,讓較長的鏈表的指針先走|m-n|個長度,若是他們下面的節點是同樣的,就說明出現了第一個公共節點。this

const getLinkLength = require('./link-length')
 function findSameNode(link1,link2){
   if(!link1 || !link2) return null;
   //計算兩個鏈表長度 
   let size1 = getLinkLength(link1);
   let size2 = getLinkLength(link2);
   let  n = size1- size2;
   let mH = null,sH = null;//mH存儲長鏈表 sH存儲短鏈表
   if(n>0){
      mH=link1;
      sH=link2
   }else{
     mH=link2;
     sH=link1
   }
   //大的先走
   n =Math.abs(n)
   while(n){
     mH = mH.next;
     n--;
   }  
   //兩個鏈表 mH 和 sH長度都相等
   while( (mH && sH) && mH.value !=sH.value){
     mH= mH.next;
     sH = sH.next;
   }
   return mH;
 }
複製代碼

使用鏈表實現大數加法

問題描述 兩個用鏈表表明的整數,其中每一個節點包含一個數字。數字存儲按照在原來整數中相反的順序,使得第一個數字位於鏈表的開頭。寫出一個函數將兩個整數相加,用鏈表形式返回和。spa

輸入
3->1->5->null
5->9->2->null,
輸出
8->0->8->null
複製代碼
  • 鏈表進行加法時候
  • 是否有進位
  • 最後結束後是否存在未遍歷完成的部
function comSumNode(link1,link2){
   if(!link1 || !link2) {
     return link1 || link2
   }
   let curr1 =link1,curr2 = link2;
   //t用來進行計算的總會 sum表示是否有進位
   let t = new createLink(),sum=0;
   while(curr1 && curr2){ 
     let value = curr2.value + curr1.value; 
     //判斷前一位是否有進位
     value = sum ? value+1 : value;
     sum = 0 
     //當前的t.value是否大於0 若是是 則表示下一位須要進位
     if(value>10){
       sum=1;
       value =value -10;
     }  
    t.add(value)
    curr2 = curr2.next;
    curr1 = curr1.next; 
   }
   //查看sum ,curr2和curr1是否存在剩餘的元素
   while(curr1){
     value = sum? curr1.value+sum:curr1.value;
     sum =0;
     if(value>=10){
       sum =1
       value = value-10
     }
     t.add(value)
     curr1= curr1.next;
   }
   while(curr2){
    value = sum? curr2.value+sum:curr2.value;
    sum =0;
    if(value>=10){
      sum =1
      value = value-10
    }
    t.add(value)
    curr2= curr2.next;
  }
  //還存在sum
  if(sum){
    t.add(sum)
  }
   return t.head;
 }
複製代碼

有序鏈表合併

題目:將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。.net

示例:
輸入:
1->2->4,
1->3->4
輸出:
1->1->2->3->4->4
複製代碼

常規作法

  • a. 遍歷兩個鏈表,比較兩個鏈表的大小
  • b. 將小的元素壓入到新的鏈表中
/** * 合併兩個有序鏈表 常規作法 * @param {*} link1 * @param {*} link2 */
function mergeLink(link1,link2){
  if(!link1 && !link2){
    return link2 || link1;
  }
  let newLink = new createLink();
  let curr1=link1,curr2=link2;
  while(curr1 && curr2){ 
    if(curr1.value > curr2.value){
      //若是link2的元素小將2的元素壓入到新鏈表中 則2的指針向後移動
      newLink.add(curr2.value);
      curr2 =curr2.next;
    }else{
      newLink.add(curr1.value);
      curr1 =curr1.next;
    }
  }
  //查看是否還存在剩餘元素
  while(curr2){
    newLink.add(curr2.value);
    curr2 = curr2.next;
  }
  while(curr1){
    newLink.add(curr1.value);
    curr1 = curr1.next;
  }
  return newLink;
}

let result = mergeLink(link2.head,link.head)
result.print()
複製代碼

遞歸思想

  • 對空鏈表存在的狀況進行處理,假如 pHead1 爲空則返回 pHead2 ,pHead2 爲空則返回 pHead1。
  • 比較兩個鏈表第一個結點的大小,肯定頭結點的位置
  • 頭結點肯定後,繼續在剩下的結點中選出下一個結點去連接到第二步選出的結點後面,而後在繼續重複(2 )(3) 步,直到有鏈表爲空。
//遞歸實現
function dgMergeLink(link1,link2){
  let newLink = new LinkNode();
  //link1 或者link2爲空 則返回對方
  if(link1 ==null){
    return link2;
  }else if(link2 ==null){
    return link1
  }else{
     //比較兩個的大小
     if(link2.value > link1.value){
       newLink = link1; //將link1壓入 繼續遍歷link1.next
       newLink.next = dgMergeLink(link1.next,link2)
     }else{
      newLink = link2; //將link2壓入 繼續遍歷link2.next
      newLink.next = dgMergeLink(link1,link2.next)
     }
     return newLink 
  } 
}
let result1 = dgMergeLink(link2.head,link.head) 
let r = new createLink();
r.header = result1;
r.print()
複製代碼

環形鏈表

單鏈表中的環是指鏈表末尾的節點的 next 指針不爲 NULL ,而是指向了鏈表中的某個節點,致使鏈表中出現了環形結構。

image.png

圖2 環形鏈表結構

對於最後一個節點8 ,並不是指向null,而是next指向了3,造成了環形結構;

判斷鏈表中是否有還

標記記錄法

遍歷鏈表,訪問過的標記爲1,若是遍歷到一個節點正好是訪問過的,則存在環,不然不存在;

/** * 判斷鏈表是否存在 環 * 窮舉法 */
function isCommonRing(link){
  let curr = link;
  while(curr){
    //已經訪問過了 則存在 表示 有環
    if(curr.isVisted){
      return true;
    }
    curr.isVisted = 1; 
    curr =curr.next
  }
  return false;
}
複製代碼

創建set表

快慢指針

  • 定義兩個指針分別爲 slow,fast,而且將指針均指向鏈表頭節點。
  • 規定,slow 指針每次前進 1 個節點,fast 指針每次前進兩個節點。
  • 當 slow 與 fast 相等,且兩者均不爲空,則鏈表存在環。
function isHaveRingFast(link){
  let slow = link;
  let fast = link;

  while(slow && fast.next){
    slow = slow.next;//慢指針
    fast = fast.next.next;//快指針
    if(slow.value == fast.value){
      return true;
    } 
  }
  return false;
}
複製代碼

找到環路的入口節點

slow 指針每次前進一個節點,故 slow 與 fast 相遇時,slow 尚未遍歷完整個鏈表。設 slow 走過節點數爲 s,fast 走過節點數爲 2s。設環入口點距離頭節點爲 a,slow 與 fast 首次相遇點距離入口點爲 b,環的長度爲 r。

image.png

圖3 環形鏈表圖
設環的長度爲r
則slow走過的路程 s=a+b
Fast走過的路程 2s = a+b+n *r (n表示走過的圈數)  
s = n * r; 意味着slow指針走過的長度爲環的長度整數倍。
若鏈表頭節點到環的末尾節點度爲 L,slow 與 fast 的相遇節點距離環入口節點爲 X。
則有:
a+X = s = n * r = (n - 1) * r + (L - a);
a = (n - 1) * r + (L - a - X);(正好指向入口節點,須要推導o)
從 slow 與 fast 相遇點出發一個指針 p1,前進 (L - a - X) 步,則此指針到達入口節點。同時指針 p2 從頭結點出發,前進 a 步。當 p1 與 p2 相遇時,此時 p1 與 p2 均指向入口節點。
(L - a - X) 是相遇點到入口點距離
複製代碼
function getMeetNode(link){
  let slow = link;
  let fast = link;
  while(slow && fast.next){
    slow = slow.next;//慢指針
    fast = fast.next.next;//快指針
    if(slow.value == fast.value){
      return slow;
    } 
  }
  return null;
}

//根據入口節點 找到入口節點
function findEntry(link){
  let node = getMeetNode(link);
  if(!node) return null;
  let p1 = node;
  let p2 = link;
  while(p1 != p2){
    p1=p1.next;
    p2=p2.next
  }
  return p2
}
複製代碼

計算環路長度

根據找到的相遇點進行計算便可; 在圖3中找到了 slow 與 fast 的相遇節點,令 solw 與 fast 指針從相遇節點出發,按照以前的前進規則,當 slow 與fast 再次相遇時,slow 走過的長度正好爲環的長度。 找到相遇點: 特地畫了流程圖

image.png

image.png

image.png

圖4 計算環形鏈表圖
function comRingLength(link){
  if(!link) return null;
  let fast = link;
  let slow = link;
  //找到相遇點
  while(fast.next && slow){
    fast =fast.next.next;
    slow = slow.next
    if(fast == slow){
      break;
    }
  }
  //繼續出發
  slow =slow.next; 
  fast = fast.next.next;
  let num = 1;
  //當它們再次相遇的時候;
  while(slow != fast){
    fast = fast.next.next;
    slow = slow.next; 
    num ++;
  }
  return num; 
}
let number = comRingLength(link.head)
console.log(number)
複製代碼

參考文檔

相關文章
相關標籤/搜索