線性表(循環(huán)鏈表)

前言

昨天說了線性表中的鏈表,相信仔細(xì)看過的同學(xué)應(yīng)該會明白 的思想,今天我們來說一說循環(huán)鏈表。


定義

什么是循環(huán)鏈表呢?

循環(huán)鏈表是一種鏈?zhǔn)酱鎯Y(jié)構(gòu),它的最后一個(gè)結(jié)點(diǎn)指向頭結(jié)點(diǎn),形成一個(gè)環(huán)。因此,從循環(huán)鏈表中的任何一個(gè)結(jié)點(diǎn)出發(fā)都能找到任何其他結(jié)點(diǎn)。

感覺這么說也不是特別好理解。給大家看圖吧,接著昨天的畫.. 別嫌棄哈。循環(huán)鏈表示意圖,如圖 1 。
圖 1 循環(huán)鏈表示意圖

我們讓鏈表中的最后一個(gè)結(jié)點(diǎn),指向頭結(jié)點(diǎn)。這樣鏈表就成了一個(gè)環(huán)(對于內(nèi)存管理比較熟悉的同學(xué)也許已經(jīng)感覺到了壞味道,因?yàn)樾纬闪谁h(huán),我的思路是,我們打破這個(gè)環(huán)就好了,而且,我們的主題是數(shù)據(jù)結(jié)構(gòu),思想為主)。

Q:使鏈表循環(huán),或者說頭尾相接的的好處是什么呢?

好處就是從循環(huán)鏈表中的任何一個(gè)結(jié)點(diǎn)出發(fā)都能找到任何其他結(jié)點(diǎn)。使用起來更加靈活。比如說,我們現(xiàn)在的鏈表,查找第一個(gè)結(jié)點(diǎn),只需要 O(1) 的時(shí)間,查找最后一個(gè)結(jié)點(diǎn)需要 O(n) 的時(shí)間,但是相信雖然我還沒寫到隊(duì)列,大家也都知道棧和隊(duì)列的原理。如果一個(gè)鏈表經(jīng)常要對第一個(gè)結(jié)點(diǎn)和最后一個(gè)結(jié)點(diǎn)進(jìn)行操作,比如棧和隊(duì)列,那么我們之前實(shí)現(xiàn)的鏈表就不是很合理,因?yàn)楂@取最后一個(gè)結(jié)點(diǎn)的時(shí)間復(fù)雜度有點(diǎn)高。

Q:說了這么多,這和循環(huán)鏈表到底有什么關(guān)系呢?

我們這里還是需要換個(gè)思路,用循環(huán)鏈表來解決這個(gè)問題。我們用最后一個(gè)結(jié)點(diǎn)來表示鏈表,最后一個(gè)結(jié)點(diǎn)的 next 稱為尾指針。比如說圖 1 中的最后一個(gè)結(jié)點(diǎn)是 node9 , node9 有數(shù)據(jù)域 data = 9, 指針域 next = 頭結(jié)點(diǎn),那么 node9 就是尾結(jié)點(diǎn) rear ,尾指針就是 rear.next (node9.next) ,也就是頭結(jié)點(diǎn)了。

通過這種表示方法,我們在獲取第一個(gè)結(jié)點(diǎn)的時(shí)候,只需要通過 rear.next.next(node9.next.next => 頭結(jié)點(diǎn).next)就可以找到第一個(gè)結(jié)點(diǎn);通過 rear 就可以找到最后一個(gè)結(jié)點(diǎn),因?yàn)?rear 就是最后一個(gè)結(jié)點(diǎn)(node9)啦。所以,這種表示方法,可以使獲取第一個(gè)結(jié)點(diǎn)和最后一個(gè)結(jié)點(diǎn)的時(shí)間復(fù)雜度為 O(1) 常數(shù)項(xiàng)。這樣,我們在用鏈表實(shí)現(xiàn)棧和隊(duì)列的時(shí)候,例如獲取棧頂、棧底、隊(duì)頭、隊(duì)尾等操作,我們可以以較高的效率來完成,而不是每次去遍歷整個(gè)表,這樣太浪費(fèi)了;而且類似于合并兩個(gè)鏈表的操作就更加省力了,只需將一個(gè)表尾和另一個(gè)表首鏈接,去掉頭就好了。

Q:在上一節(jié)中的鏈表實(shí)現(xiàn)中,我們用 node.next? 來判斷鏈表是否到了表尾、鏈表是否為空。但是循環(huán)鏈表的 next 一定不為空 (循環(huán)嘛),我們怎么判斷呢?

先想象一下,然后我們來看一張,空的循環(huán)鏈表的圖吧。
圖 2 空的循環(huán)鏈表

如圖 2 所示,當(dāng)循環(huán)鏈表為空的時(shí)候,頭結(jié)點(diǎn)的指針域(next)指向頭結(jié)點(diǎn)自己,構(gòu)成循環(huán)。但是尾結(jié)點(diǎn)呢?沒錯(cuò),尾結(jié)點(diǎn)就是頭結(jié)點(diǎn)。所以就有了如下的判斷條件:

1. head.next == head;
2. rear.next == rear; //head == rear

第一種情況我們已經(jīng)說明了,就是頭結(jié)點(diǎn)的指針域(next)指向頭結(jié)點(diǎn)自己;
第二種情況就是, 鏈表為空的時(shí)候,頭結(jié)點(diǎn)就是尾結(jié)點(diǎn)(沒明白的同學(xué)好好理解,反正我這蠢人,理解了半天才反應(yīng)過來...),也可以說是頭尾重合了,那么鏈表為空。

我發(fā)現(xiàn)使用 Objective-C 來實(shí)現(xiàn)循環(huán)鏈表感覺真心有點(diǎn)煩。原因有如下兩點(diǎn):

1. 因?yàn)檠h(huán)鏈表是個(gè)環(huán),大家肯定也很容易想到循環(huán)引用,對的,我們要處理保留環(huán);
2. 因?yàn)橛梦步Y(jié)點(diǎn)來表示循環(huán)鏈表,所以,我們在插入、刪除、整表創(chuàng)建的時(shí)候都要去修改尾結(jié)點(diǎn),但是像上一節(jié)中的鏈表那樣直接把鏈表本身看成頭結(jié)點(diǎn)就不行了,因?yàn)闆]辦法修改 self (即 self = node 是不允許的)。

循環(huán)鏈表的實(shí)現(xiàn)

來看代碼吧:

循環(huán)鏈表 header:

/// 循環(huán)鏈表還是繼承于線性表,其實(shí)現(xiàn)中的 node 也是用的昨天的 node。如果有沒看到,或者沒看的同學(xué),傳送門在這里(本來是想寫在代碼中的,不過貌似有點(diǎn)沖突.. 就提出來了)。

#import "CHRLinearList.h"

@interface CHRSinglyCircularLinkedList : CHRLinearList
@end

循環(huán)鏈表實(shí)現(xiàn):

#import "CHRSinglyCircularLinkedList.h"
#import "CHRSinglyLinkedListNode.h"

@interface CHRSinglyCircularLinkedList ()

@property (nonatomic, strong) CHRSinglyLinkedListNode *rear; /// 這里的 rear 是指尾結(jié)點(diǎn),如果鏈表為空, rear 和 頭結(jié)點(diǎn) head 就重合了,也就是說當(dāng)鏈表為空, rear == head

@property (nonatomic, assign) NSUInteger count; /// 個(gè)數(shù),在循環(huán)鏈表的實(shí)現(xiàn)中,每次鏈表長度的修改,都去計(jì)算了 count 值 , 而不是每次都去遍歷整個(gè)表才能得到 count,提高了 count 的查詢效率

@end

@implementation CHRSinglyCircularLinkedList

///  合成屬性 count
@synthesize count = _count;

- (void)dealloc
{
  /* 
      重寫了 dealloc, 打破保留環(huán)
      如果不明白為什么的同學(xué),自己查文檔吧,傳送門都不給了 
    */
  _rear.next = nil;
}

- (nonnull instancetype)initWithObjects:(nullable id)objects,...
{
  self = [super init];
  if (self) {
    
    /* 
      整表創(chuàng)建的時(shí)候,和鏈表很相似,大概就是  
      
      1. 如果有數(shù)據(jù),構(gòu)造結(jié)點(diǎn),把數(shù)據(jù)包裝到結(jié)點(diǎn)中
      2. prior 是上一個(gè)結(jié)點(diǎn),讓上一個(gè)結(jié)點(diǎn)的指針域 next 指向新的結(jié)點(diǎn)
      3. 更新 prior 為新的結(jié)點(diǎn)

      直到循環(huán)結(jié)束, prior 結(jié)點(diǎn)已經(jīng)變成了 rear 結(jié)點(diǎn),如果鏈表是空的,那么 prior 既是 head 也是 rear, 也就是空鏈表了
      對了,千萬別忘記 rear.next = head, 構(gòu)成循環(huán)
    */
    
    _count = 0; /// 初始化 count 為0,表中什么都沒有
    
    id <CHRSinglyLinkedListNodeProtocol> head = [[CHRSinglyLinkedListNode alloc] init]; /// 頭指針
    id <CHRSinglyLinkedListNodeProtocol> prior = head;
    
    va_list params;
    va_start(params, objects);
    id object = objects;
    
    while (object) {
      CHRSinglyLinkedListNode *node = [[CHRSinglyLinkedListNode alloc] init];
      node.data = object;
      prior.next = node;
      prior = node;
      _count++;
      object = va_arg(params, id);
    }
    
    va_end(params);
    
    _rear = prior;
    _rear.next = head; 
  }
  return self;
}

- (BOOL)isEmpty
{
  /// 判空條件,如果 rear.next (head) == rear, 鏈表為空,頭尾重合了
  return self.rear.next == self.rear;
}

- (NSUInteger)indexOfObject:(id)object
{
  /*
      聲明一個(gè) index 為 0
      找到頭結(jié)點(diǎn)
      聲明一個(gè) node 指針指向頭結(jié)點(diǎn)
      如果頭尾沒有重合,開始循環(huán)
      
      如果 node (頭結(jié)點(diǎn)).next.data 等同于 object, 也就是第一個(gè)結(jié)點(diǎn)的數(shù)據(jù)域如果與 object 相同,那么返回 index(0) 
      如果不相同, index 自增,進(jìn)入下次循環(huán)
      ...
      
      如果在鏈表中存在 Object,那么在上面的鏈表的遍歷中,就已經(jīng)返回了 index
      如果過了循環(huán)還沒有返回,說明鏈表中不存在與這個(gè) object 等同的數(shù)據(jù),那么返回 NSNotFound
  */
  NSUInteger index = 0;
  id <CHRSinglyLinkedListNodeProtocol> node = self.rear.next;
  while (node != self.rear) {
    node = node.next;
    if ([node.data isEqual:object]) {
      return index;
    }
    index++;
  }
  return NSNotFound;
}

- (id)objectAtIndex:(NSUInteger)index
{
  /// 如果給定的 index 超出了鏈表的范圍,崩潰
  NSAssert(index < self.count, @"%s, %d 線性表越界, 當(dāng)前線性表共有 %@ 個(gè)元素",  __FILE__, __LINE__, @(self.count));
  
  id <CHRSinglyLinkedListNodeProtocol> node = self.rear.next.next;
  NSUInteger ctrIndex = 0;
  while (ctrIndex < index) {
    node = node.next;
    ctrIndex++;
  }

  return node.data;
}

- (void)insertObject:(id)object atIndex:(NSUInteger)index
{
  NSAssert(object, @"%s, %d, 向線性表中插入了 nil 是不允許的", __FILE__, __LINE__);
  NSAssert(index <= self.count, @"%s, %d 線性表越界, 當(dāng)前線性表共有 %@ 個(gè)元素",  __FILE__, __LINE__, @(self.count));
  
  ///  如果上面斷言成功,那么就是合法的插入
  
  id <CHRSinglyLinkedListNodeProtocol> prior = self.rear.next;
  NSUInteger ctrIndex = 0;
  
  while (ctrIndex < index) {
    prior = prior.next;
    ctrIndex++;
  }
  
  CHRSinglyLinkedListNode *node = [[CHRSinglyLinkedListNode alloc] init];
  node.data = object;
  
  node.next = prior.next;
  prior.next = node;
  
  self.count++; /// 更新 count
  
  /// 插入操作和鏈表幾乎一致,需要注意的是,有判斷是否要更新 尾結(jié)點(diǎn)(self.rear),因?yàn)橛脩粲锌赡茉诒砦膊迦?  /*  
      有的同學(xué)可能會擔(dān)心如果 prior 是尾結(jié)點(diǎn)了,那么循環(huán)怎么處理呢?不用擔(dān)心,看下我的分析
      如果 prior 是尾結(jié)點(diǎn),那么 node.next = prior.next,這個(gè)時(shí)候新插入的結(jié)點(diǎn)的指針域 next 指向 prior 的 next 也就是頭結(jié)點(diǎn)
      然后我們這樣 prior.next = node ,打破了原來尾結(jié)點(diǎn)的環(huán),讓尾結(jié)點(diǎn)的指針域指向新的結(jié)點(diǎn),構(gòu)成循環(huán)
  */
  
  if (prior == self.rear) { /// 更新 rear 指針
    self.rear = node;
  }
}

- (id)removeObjectAtIndex:(NSUInteger)index
{
  ///  刪除和插入差不多,我就不多說了,相信好好看的同學(xué),已經(jīng)看懂了
  NSAssert(index < self.count, @"%s, %d 線性表越界, 當(dāng)前線性表共有 %@ 個(gè)元素",  __FILE__, __LINE__, @(self.count));
  
  NSUInteger ctrIndex = 0;
  
  id <CHRSinglyLinkedListNodeProtocol> prior = self.rear.next;
  
  while (ctrIndex < index) {
    prior = prior.next;
    ctrIndex++;
  }
  
  id <CHRSinglyLinkedListNodeProtocol> node = prior.next; /// 要?jiǎng)h除的節(jié)點(diǎn)
  prior.next = node.next;
  node.next = nil;
  
  self.count--;
  
  if (node == self.rear) { /// 更新尾指針
    self.rear = prior;
  }
  
  return node.data;
}

- (BOOL)containsObject:(id)object
{
  ///  這個(gè)也就不說了吧,遍歷整張表,如果結(jié)點(diǎn)的數(shù)據(jù)域 data 等同于 object ,理解返回 YES;如果循環(huán)結(jié)束,沒有返回,那么說明不包含 object , 返回 NO
  id <CHRSinglyLinkedListNodeProtocol> node = self.rear.next;
  while (node != self.rear) {
    node = node.next;
    if ([node.data isEqual:object]) {
      return YES;
    }
  }
  return NO;
}

- (void)removeAllObjects
{
  ///  找到頭結(jié)點(diǎn) head
  id <CHRSinglyLinkedListNodeProtocol> head = self.rear.next;
  
  /*
      我這邊是這樣的思路
  
      一直刪除第一個(gè)結(jié)點(diǎn), 也就是 head.next(就算 head.next 是尾結(jié)點(diǎn),那么 head.next = node.next , 也就變成了空鏈表的情況,如圖 2 )
      然后除了循環(huán)之后, 更新 rear 和 count
  */
  while (head != head.next) {
    id <CHRSinglyLinkedListNodeProtocol> node = head.next; /// 要?jiǎng)h除的節(jié)點(diǎn)
    head.next = node.next;
    node.next = nil;
  }
  self.rear = head;
  self.count = 0;
}

@end

End

大概的代碼就是這些,相信思路大家已經(jīng)有了大致了解,我覺得只要整整理解了空表的情況, 也就是 rear == head == read.next ,那么就應(yīng)該很好理解了。我在看書的時(shí)候,看懵逼了好多遍, 實(shí)現(xiàn)了三天,才真正理解了,唉,我這麻瓜。

對了,劇透一下,現(xiàn)在我們實(shí)現(xiàn)了 鏈表、循環(huán)鏈表, 但是,我們實(shí)現(xiàn)的是單(向)鏈表。既然有單向鏈表,那么就有雙向鏈表咯,其實(shí)很簡單,雙向鏈表就是加一個(gè)指針域 prior ,使用起來更加靈活,某些時(shí)候可以達(dá)到更高的效率,我的理解就是用空間來換時(shí)間。雙鏈表也可以循環(huán),其實(shí)道理很類似,我們下一篇再說。

還有,這篇昨天寫了一半,今天上午寫了一半,可能有一些思路斷掉了,看不懂,或者有錯(cuò)誤的話,大家請指正。本文的代碼,還是放在那個(gè)倉庫中,歡迎大家測試,找 bug ,尤其是內(nèi)存問題。

好了,先到這里了...
Im Chris, bye ~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容