寫在前邊
暑假參加的第一個公司的就讓我手寫一個雙向鏈表,並完成插入數據和刪除數據的操作。當時我很矇蔽,懵逼的不是思路,而是手寫,雖然寫出來了,但是很多邊界條件和代碼規範自我感覺不好,所以有了這些細心的總結。那麼今天的主題就是徒手寫鏈表,應聘者該如何下手?
我們通常寫鏈表準備應聘的時候,通常背加上理解,但是過了幾天又讓你寫。就會陌生了,雖然有點思路。還是模模糊糊,小鹿也有這個記性的“毛病”,“有毛病”就要治,怎麼治?我們必須在腦海裏形成一套可行的步驟和方法,在遇到手寫就不用手忙腳亂,而是穩穩當當,從頭到尾寫出一個漂亮的鏈表結構及操作。
一、熟悉結構
首先我們要知道鏈表的結構以及每個節點的結構,這是我們手寫鏈表的第一步,也是學習鏈表的第一步。我們知道,每個鏈表時這樣表示的:
那每個節點結構是由數據域和指針域組成,數據域是存放數據的,而指針域存放下一結點的地址。
我們可以通過數據域訪問到我們要的數據,而通過指針域訪問到當前結點以後的結點,那麼將這些結點串起來,就是一個鏈表。
那麼用代碼怎麼來表示呢?
我們通常會用到函數,我們如果將一個函數抽象成一個結點,那麼我們給函數添加兩個屬性,一個屬性是存放數據的屬性data,另一個屬性是存放指向下一個結點的指針屬性next。
你可能會問,如果我們有成千上萬個結點,要定義成千上萬個函數?有一個函數叫做構造函數,想必大家都聽說過,聲明一個構造函數就可以創造出成千上萬個結點實例。
function Node(data){
this.data = data;
this.next = null;
}
還有一個方法就是使用類的概念,在JavaScript中,類的概念在ES6纔出現,使用 Class 來聲明一個類,我們可以爲類添加兩個屬性,同上,一樣可以創造出多個結點實例。
class Node{
constructor(data){
this.data = data;
this.next = null;
}
}
二、理清思路
如果你把鏈表的結構搞得明明白白了,恭喜你,成功的進入第二關,但是你只超越了百分之20的人,繼續加油。
既然鏈表的結構弄明白了,那麼我們開始理思路,我們就先拿最簡單的單鏈表開刀,我們要完成兩個操作,插入數據和刪除數據。
如果我想插入數據,你可能會問,往哪裏插呢?有幾種插入的方法?
開始想,插入到單鏈表的頭部算一種。
然後插入到單鏈表的中間算一種。
插入到單鏈表尾部又算一種。
所有可能的情況就三種。那麼刪除呢?想必你也想到了,也一共三種,刪除頭部、刪除中間部分、刪除尾部。
PS:這是後期增加。
大家有沒有想過如果增加的結點是第一個結點,也就是在空鏈表中添加新結點的情況呢?所以寫代碼的時候要把這種特殊情況考慮進去。
然後還有刪除的情況,如果爲空鏈表了,就不能刪除了。如果刪除的是頭結點,還需要把頭指針向後移動一個,因爲當前的頭結點被刪除無效。
如果你覺的現在可以寫代碼了,那你就錯了,雖然我們的思路非常清晰,但是面試官僅僅考我們思路嗎?其實這一關你只打敗了百分之50%的人,最重點、最主要的是在下一個部分,邊界條件。
三、邊界條件
邊界條件是這五個步驟最容易犯錯的一部分,因爲通常考慮的不全面,導致了最後的面試未通過。如果想做好這一部分,也不難,跟着小鹿的方法走。
1、輸入邊界
首先我們先考慮用戶輸入的參數,比如傳入一個鏈表,我們首先要判斷鏈表是否爲空,如果爲空我們就不能讓它執行下邊的程序。再比如插入一個結點到指定結點的後邊,那麼你也要判斷輸入的結點是否爲空,而且還要判斷該結點是否存在該鏈表中。對於這些輸入值的判斷,小鹿給他同一起個名字叫做輸入邊界。
2、特殊邊界
特殊邊界考慮到一些特殊情況,比如插入數據,我們插入數據一般考慮到插入尾部,但是突然面試官插入到頭部,插入尾部的代碼並不適用於插入到頭部,所以呢需要考慮這種情況,刪除節點也是同樣思考。其實特殊邊界最主要考慮到一些邏輯上的特殊情況,考察應聘者的考慮的是否全面。小鹿給他起個名字叫做特殊邊界。
四、手寫代碼
1、定義結點
class Node{
constructor(data){
this.data = data;
this.next = null;
}
}
2、增加結點
咱們就以單鏈表中部添加數據爲例子,分解成每個步驟,每個步驟對應代碼如下:
2.1 保存臨時地址(4結點的地址),需要進行遍歷查找到3結點,也就是下列代碼的currentNode 結點。
//先查找該元素
let currentNode = this.findByValue(element);
// 保存 3 結點的下一結點地址(4 結點的地址)
let pre = currentNode.next
2.2 創建新結點,將新結點(5結點)的指針指向下一結點指針(4結點地址,已經在上一步驟保存下來了)
let newNode = new Node(value);
newNode.next = pre;
2.3 將3 的結點地址指向新結點(5結點)
currentNode.next = newNode;
以上步驟分析完畢,剩下的兩個在頭部插入和在尾部插入同樣的分析方式,將這兩個作爲練習題,課下自己試一試這個步驟。
2.4 刪除節點
刪除節點也分爲三種,頭部、中部、尾部,咱們就刪除中間結點爲例進行刪除,我們詳細看步驟操作。
我們先看刪除的全部動畫,然後再分步拆分。
斷開3結點的指針(斷開3結點相當於讓2結點直接指向4結點)
let currentNode = this.head;
// 用來記錄 3 結點的前一結點
let preNode = null;
// 遍歷查找 3 結點
while(currentNode !== null && currentNode.data !== value){
// 3 結點的前一結點
preNode = currentNode;
// 3 結點
currentNode = currentNode.next;
}
讓結點2的指針指向4結點,完成刪除。
preNode.next = currentNode.next;
剩下的刪除頭結點和刪除尾結點同樣的步驟,自己動手嘗試下。
所有代碼實現:
1/**
2 * 2019/3/23
3 * 公衆號:「一個不甘平凡的碼農」
4 * @author 小鹿
5 * 功能:單鏈表的插入、刪除、查找
6 * 【插入】:插入到指定元素後方
7 * 1、查找該元素是否存在?
8 * 2、沒有找到返回 -1
9 * 3、找到進行創建結點並插入鏈表。
10 *
11 * 【查找】:按值查找/按索引查找
12 * 1、判斷當前結點是否等於null,且是否等於給定值?
13 * 2、判斷是否可以找到該值?
14 * 3、沒有找到返回 -1;
15 * 4、找到該值返回結點;
16 *
17 * 【刪除】:按值刪除
18 * 1、判斷是否找到該值?
19 * 2、找到記錄前結點,進行刪除;
20 * 3、找不到直接返回-1;
21 */
22//定義結點
23class Node{
24 constructor(data){
25 this.data = data;
26 this.next = null;
27 }
28}
29
30//定義鏈表
31class LinkList{
32 constructor(){
33 //初始化頭結點
34 this.head = new Node('head');
35 }
36
37 //根據 value 查找結點
38 findByValue = (value) =>{
39 let currentNode = this.head;
40 while(currentNode !== null && currentNode.data !== value){
41 currentNode = currentNode.next;
42 }
43 //判斷該結點是否找到
44 console.log(currentNode)
45 return currentNode === null ? -1 : currentNode;
46 }
47
48 //根據 index 查找結點
49 findByIndex = (index) =>{
50 let pos = 0;
51 let currentNode = this.head;
52 while(currentNode !== null && pos !== index){
53 currentNode = currentNode.next;
54 pos++;
55 }
56 //判斷是否找到該索引
57 console.log(currentNode)
58 return currentNode === null ? -1 : currentNode;
59 }
60
61 //插入元素(指定元素向後插入)
62 insert = (value,element) =>{
63 //先查找該元素
64 let currentNode = this.findByValue(element);
65 //如果沒有找到
66 if(currentNode == -1){
67 console.log("未找到插入位置!")
68 return;
69 }
70 let newNode = new Node(value);
71 newNode.next = currentNode.next;
72 currentNode.next = newNode;
73 }
74
75 //根據值刪除結點
76 delete = (value) =>{
77 let currentNode = this.head;
78 let preNode = null;
79 while(currentNode !== null && currentNode.data !== value){
80 preNode = currentNode;
81 currentNode = currentNode.next;
82 }
83 if(currentNode == null) return -1;
84 preNode.next = currentNode.next;
85 }
86
87 //遍歷所有結點
88 print = () =>{
89 let currentNode = this.head
90 //如果結點不爲空
91 while(currentNode !== null){
92 console.log(currentNode.data)
93 currentNode = currentNode.next;
94 }
95 }
96}
97
98//測試
99const list = new LinkList()
100list.insert('xiao','head');
101list.insert('lu','xiao');
102list.insert('ni','head');
103list.insert('hellow','head');
104list.print()
105console.log('-------------刪除元素------------')
106list.delete('ni')
107list.delete('xiao')
108list.print()
109console.log('-------------按值查找------------')
110list.findByValue('lu')
111console.log('-------------按索引查找------------')
112list.print()
五、測試用例
其實這裏的測試用例主要用於判斷我們寫的程序到底對不對,我們一般都會輸入一個自己認爲的情況進行測試,這是錯誤的做法。程序最容易出錯的還是邊界情況考慮不全面導致的,所以,我們爲了能夠測試程序的全面性,所以我們要按照以下步驟進行全面性的測試。
1、普通測試
普通測試就是輸入一個正常的值,比如單鏈表中插入數據
2、特殊測試
特殊輸入可以參照上邊邊界條件中的特殊邊界進行測試,比如在頭部插入數據,在尾部插入數據等特殊情況的測試。
3、輸入測試
對於輸入測試的內容參考上邊邊界條件中的輸入邊界,比如:輸入一個空鏈表測試一下程序能否正常的運行。
六、小結
做一個小結。今天的手寫鏈表主要從五部分下手,從前到後依次爲熟悉結構、理清思路、手寫代碼、測試用例。以後無論手寫什麼代碼,有五步走,對於面試完全沒有問題啦。
通過小鹿總結手寫鏈表的方法,不用刻意去背,只要把思路理清楚,邊界條件考慮全面,就不用去背,重複的練習。
下一篇:別再翻了,面試二叉樹看這 11 個就夠了~
推薦閱讀:
1、動畫:用動畫給面試官解釋 TCP 三次握手過程
2、動畫:用動畫給女朋友講解 TCP 四次分手過程
❤️ 不要忘記留下你學習的腳印 [點贊 + 收藏 + 評論]
文章都看完了,爲何不妨點個贊呢?嘻嘻,那就說明你很自私,你怕那麼好的文章讓別人也看到。開個小小玩笑。
其實我也很自私,我把我的一直以來堅持原創的公衆號:「小鹿動畫學編程」偷偷給你,裏邊匯聚了小鹿以動畫形式講解的數據結構與算法、網絡原理、Web 等技術文章。
動一動你的小手,點贊就完事了,每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=
作者Info:
【作者】:小鹿
【原創公衆號】:小鹿動畫學編程。
【簡介】:和小鹿同學一起用動畫的方式從零基礎學編程,將 Web前端領域、數據結構與算法、網絡原理等通俗易懂的呈獻給小夥伴。先定個小目標,原創 1000 篇的動畫技術文章,和各位小夥伴共同努力一起學習!公衆號回覆 “資料” 送一從零自學資料大禮包!
【轉載說明】:轉載請說明出處,謝謝合作!~