數據結構與算法之美學習筆記:第七講

技巧一:理解指針或引用的含義

一、指針和引用有什麼關係

二、代碼實現

技巧二:警戒指針丟失和內存泄漏

一、指針是如何弄丟的呢?

我拿單鏈表的插入操做爲例來給你分析一下node

如圖所示,咱們但願在結點a和相鄰的結點b之間插入結點x,假設當前指針p指向結點a。若是咱們將代碼實現變成下面這個樣子,就會發⽣指針丟失和內存泄露。python

p->next = x;  // 將 p 的 next 指針指向 x 結點;
x->next = p->next;  // 將 x 的結點的 next 指針指向 b 結點;

二、插入結點時、必定要注意操做的順序

三、刪除鏈表結點時、必定要手動釋放內存空間

技巧三:利用哨兵簡化實現難度

一、發現問題

一、在結點P後面插入一個新的結點

new_node->next = p->next;
p->next = new_node;

二、向一個空鏈表中插入第一個結點

剛剛的邏輯就不能用了、須要進行下面這樣的特殊處理、其中head表示鏈表的頭結點、對於單鏈表的插入操做,第一個結點和其餘結點的插入邏輯是不同面試

if (head == null) {
  head = new_node;
}

三、單鏈表的結點刪除操做

若是要刪除結點P的後繼結點,咱們只須要一行代碼就能夠搞定編程

p->next = p->next->next;

四、刪除鏈表中的最後一個結點

跟插入相似數組

if (head->next == null) {
   head = null;
}

從前面的一步一步分析,咱們能夠看出,針對鏈表的插入、刪除操做,須要對插入第一個結點和刪除最後一個結點的狀況進行特殊處理。這樣代碼實現起來就會很繁瑣,不簡潔,並且也容易由於考慮不全而出錯。如何來解決這個問題呢?性能

二、解決問題

一、什麼是哨兵

二、帶頭的鏈表和不帶頭的鏈表

我畫了一個帶頭鏈表,你能夠發現,哨兵結點是不存儲數據的。由於哨兵結點一直存在,因此插入第一個結點和插入其餘結點,刪除最後一個結點和刪除其餘結點,均可以統一爲相同的代碼實現邏輯了。spa

 

三、哨兵簡化了編程的難度

我再舉一個很是簡單的例子。代碼我是用C語言語實現的,不涉及語言方面的高級語法、很容易看懂,你能夠類比到你熟悉的語。3d

代碼一指針

// 在數組 a 中,查找 key,返回 key 所在的位置
// 其中,n 表示數組 a 的長度
int find(char* a, int n, char key) {
  // 邊界條件處理,若是 a 爲空,或者 n<=0,說明數組中沒有數據,就不用 while 循環比較了
  if(a == null || n <= 0) {
    return -1;
  }
  
  int i = 0;
  // 這裏有兩個比較操做:i<n 和 a[i]==key.
  while (i < n) {
    if (a[i] == key) {
      return i;
    }
    ++i;
  }
  
  return -1;
}

代碼二調試

// 在數組 a 中,查找 key,返回 key 所在的位置
// 其中,n 表示數組 a 的長度
// 我舉 2 個例子,你能夠拿例子走一下代碼
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 7
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 6
int find(char* a, int n, char key) {
  if(a == null || n <= 0) {
    return -1;
  }
  
  // 這裏由於要將 a[n-1] 的值替換成 key,因此要特殊處理這個值
  if (a[n-1] == key) {
    return n-1;
  }
  
  // 把 a[n-1] 的值臨時保存在變量 tmp 中,以便以後恢復。tmp=6。
  // 之因此這樣作的目的是:但願 find() 代碼不要改變 a 數組中的內容
  char tmp = a[n-1];
  // 把 key 的值放到 a[n-1] 中,此時 a = {4, 2, 3, 5, 9, 7}
  a[n-1] = key;
  
  int i = 0;
  // while 循環比起代碼一,少了 i<n 這個比較操做
  while (a[i] != key) {
    ++i;
  }
  
  // 恢復 a[n-1] 原來的值, 此時 a= {4, 2, 3, 5, 9, 6}
  a[n-1] = tmp;
  
  if (i == n-1) {
    // 若是 i == n-1 說明,在 0...n-2 之間都沒有 key,因此返回 -1
    return -1;
  } else {
    // 不然,返回 i,就是等於 key 值的元素的下標
    return i;
  }
}

對比兩段代碼,在字符串a很⻓的時候,好比幾萬、幾十萬,你以爲哪段代碼運行得更快點呢?答案是代碼二,由於兩段代碼中執行次數最多就是while循環那一部分。第二段代碼中,咱們經過⼀個哨兵a[n-1] = key
,成功省掉了一個個⽐較語句i<n,不要小看這一條語句,當累積執行萬次、幾十萬次時,累積的時間就很明顯了。

固然,這只是爲了舉例說明哨兵的做用,你寫代碼的時候千萬不要寫第⼆段那樣的代碼,由於可讀性太差了。大部分狀況下,咱們並不須要如此追求極致的性能。

技巧四:重點留意邊界條件處理

一、咱們常常用來檢查鏈表代碼是否正確的邊界條件有這樣幾個

二、針對不一樣的場景

技巧五:舉例畫圖、輔助思考

你能夠找個個具體的例子,把它畫在紙上,釋放一些腦容量,留更多的給邏輯思考,這樣就會感受到思路清晰不少。好比往單鏈表中插入一個數據這樣一個操做,我通常都是把各類狀況都舉一個例子,畫
出插入前和插入後的鏈表變化,如圖所示:

看圖寫代碼,是否是就簡單多啦,並且咱們寫完代碼以後,也能夠舉幾個例子、畫在紙上,照着代碼走一遍,很容易就能發現代碼中的Bug

技巧六:多寫多練,沒有捷徑

若是你已經理解並掌握了我前面所講的方法法,可是手寫鏈表代碼仍是會出現各類各樣的錯誤,也不要着急。由於我最開始學的時候,這種情況也持續了一段時間。

如今我寫這些代碼,簡直就和「玩兒」同樣,其實也沒有什麼技巧,就是把常見的鏈表操做都本身多寫幾遍,出問題就一點一點調試,熟能生巧!

因此,我精選了5個常⻅的鏈表操做。你只要把這幾個操做都能寫熟練,不熟就多寫幾遍,我保證你以後不再會懼怕寫鏈表代碼。

  1. 單鏈表反轉
  2. 鏈表中環的檢測
  3. 兩個有序的鏈表合併
  4. 刪除鏈表倒數第n個結點
  5. 求鏈表的中間結點


我以爲寫鏈表代碼是最考驗邏輯思惟能力的,由於鏈表代碼導出都是指針的操做、邊界條件的處理,稍有不慎就容易產生Bug鏈表代碼寫的好壞,能夠看出一我的寫代碼是否夠細心,

考慮問題是否全面、思惟是否縝密、因此,這也是不少面試官喜歡讓人手寫鏈表代碼的緣由,因此,這一節講到的東西,你必定要本身寫代碼實現一下,纔有效果

相關文章
相關標籤/搜索