線性表(循環(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é)點(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é)點(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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市本砰,隨后出現(xiàn)的幾起案子碴裙,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舔株,死亡現(xiàn)場離奇詭異莺琳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)载慈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門惭等,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人办铡,你說我怎么就攤上這事辞做。” “怎么了寡具?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵秤茅,是天一觀的道長。 經(jīng)常有香客問我童叠,道長框喳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任厦坛,我火速辦了婚禮五垮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粪般。我一直安慰自己拼余,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布亩歹。 她就那樣靜靜地躺著匙监,像睡著了一般。 火紅的嫁衣襯著肌膚如雪小作。 梳的紋絲不亂的頭發(fā)上亭姥,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音顾稀,去河邊找鬼达罗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛静秆,可吹牛的內(nèi)容都是我干的粮揉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼抚笔,長吁一口氣:“原來是場噩夢啊……” “哼扶认!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起殊橙,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤辐宾,失蹤者是張志新(化名)和其女友劉穎狱从,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叠纹,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡季研,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了誉察。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片与涡。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖冒窍,靈堂內(nèi)的尸體忽然破棺而出递沪,到底是詐尸還是另有隱情豺鼻,我是刑警寧澤综液,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站儒飒,受9級特大地震影響谬莹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桩了,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一附帽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧井誉,春花似錦蕉扮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至在岂,卻和暖如春奔则,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔽午。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工易茬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人及老。 一個(gè)月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓抽莱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親骄恶。 傳聞我的和親對象是個(gè)殘疾皇子食铐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

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