线性表(循环链表)

前言

昨天说了线性表中的链表,相信仔细看过的同学应该会明白 的思想,今天我们来说一说循环链表。


定义

什么是循环链表呢?

循环链表是一种链式存储结构,它的最后一个结点指向头结点,形成一个环。因此,从循环链表中的任何一个结点出发都能找到任何其他结点。

感觉这么说也不是特别好理解。给大家看图吧,接着昨天的画.. 别嫌弃哈。循环链表示意图,如图 1 。
图 1 循环链表示意图

我们让链表中的最后一个结点,指向头结点。这样链表就成了一个环(对于内存管理比较熟悉的同学也许已经感觉到了坏味道,因为形成了环,我的思路是,我们打破这个环就好了,而且,我们的主题是数据结构,思想为主)。

Q:使链表循环,或者说头尾相接的的好处是什么呢?

好处就是从循环链表中的任何一个结点出发都能找到任何其他结点。使用起来更加灵活。比如说,我们现在的链表,查找第一个结点,只需要 O(1) 的时间,查找最后一个结点需要 O(n) 的时间,但是相信虽然我还没写到队列,大家也都知道栈和队列的原理。如果一个链表经常要对第一个结点和最后一个结点进行操作,比如栈和队列,那么我们之前实现的链表就不是很合理,因为获取最后一个结点的时间复杂度有点高。

Q:说了这么多,这和循环链表到底有什么关系呢?

我们这里还是需要换个思路,用循环链表来解决这个问题。我们用最后一个结点来表示链表,最后一个结点的 next 称为尾指针。比如说图 1 中的最后一个结点是 node9 , node9 有数据域 data = 9, 指针域 next = 头结点,那么 node9 就是尾结点 rear ,尾指针就是 rear.next (node9.next) ,也就是头结点了。

通过这种表示方法,我们在获取第一个结点的时候,只需要通过 rear.next.next(node9.next.next => 头结点.next)就可以找到第一个结点;通过 rear 就可以找到最后一个结点,因为 rear 就是最后一个结点(node9)啦。所以,这种表示方法,可以使获取第一个结点和最后一个结点的时间复杂度为 O(1) 常数项。这样,我们在用链表实现栈和队列的时候,例如获取栈顶、栈底、队头、队尾等操作,我们可以以较高的效率来完成,而不是每次去遍历整个表,这样太浪费了;而且类似于合并两个链表的操作就更加省力了,只需将一个表尾和另一个表首链接,去掉头就好了。

Q:在上一节中的链表实现中,我们用 node.next? 来判断链表是否到了表尾、链表是否为空。但是循环链表的 next 一定不为空 (循环嘛),我们怎么判断呢?

先想象一下,然后我们来看一张,空的循环链表的图吧。
图 2 空的循环链表

如图 2 所示,当循环链表为空的时候,头结点的指针域(next)指向头结点自己,构成循环。但是尾结点呢?没错,尾结点就是头结点。所以就有了如下的判断条件:

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

第一种情况我们已经说明了,就是头结点的指针域(next)指向头结点自己;
第二种情况就是, 链表为空的时候,头结点就是尾结点(没明白的同学好好理解,反正我这蠢人,理解了半天才反应过来...),也可以说是头尾重合了,那么链表为空。

我发现使用 Objective-C 来实现循环链表感觉真心有点烦。原因有如下两点:

1. 因为循环链表是个环,大家肯定也很容易想到循环引用,对的,我们要处理保留环;
2. 因为用尾结点来表示循环链表,所以,我们在插入、删除、整表创建的时候都要去修改尾结点,但是像上一节中的链表那样直接把链表本身看成头结点就不行了,因为没办法修改 self (即 self = node 是不允许的)。

循环链表的实现

来看代码吧:

循环链表 header:

/// 循环链表还是继承于线性表,其实现中的 node 也是用的昨天的 node。如果有没看到,或者没看的同学,传送门在这里(本来是想写在代码中的,不过貌似有点冲突.. 就提出来了)。

#import "CHRLinearList.h"

@interface CHRSinglyCircularLinkedList : CHRLinearList
@end

循环链表实现:

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

@interface CHRSinglyCircularLinkedList ()

@property (nonatomic, strong) CHRSinglyLinkedListNode *rear; /// 这里的 rear 是指尾结点,如果链表为空, rear 和 头结点 head 就重合了,也就是说当链表为空, rear == head

@property (nonatomic, assign) NSUInteger count; /// 个数,在循环链表的实现中,每次链表长度的修改,都去计算了 count 值 , 而不是每次都去遍历整个表才能得到 count,提高了 count 的查询效率

@end

@implementation CHRSinglyCircularLinkedList

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

- (void)dealloc
{
  /* 
      重写了 dealloc, 打破保留环
      如果不明白为什么的同学,自己查文档吧,传送门都不给了 
    */
  _rear.next = nil;
}

- (nonnull instancetype)initWithObjects:(nullable id)objects,...
{
  self = [super init];
  if (self) {
    
    /* 
      整表创建的时候,和链表很相似,大概就是  
      
      1. 如果有数据,构造结点,把数据包装到结点中
      2. prior 是上一个结点,让上一个结点的指针域 next 指向新的结点
      3. 更新 prior 为新的结点

      直到循环结束, prior 结点已经变成了 rear 结点,如果链表是空的,那么 prior 既是 head 也是 rear, 也就是空链表了
      对了,千万别忘记 rear.next = head, 构成循环
    */
    
    _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
{
  /*
      声明一个 index 为 0
      找到头结点
      声明一个 node 指针指向头结点
      如果头尾没有重合,开始循环
      
      如果 node (头结点).next.data 等同于 object, 也就是第一个结点的数据域如果与 object 相同,那么返回 index(0) 
      如果不相同, index 自增,进入下次循环
      ...
      
      如果在链表中存在 Object,那么在上面的链表的遍历中,就已经返回了 index
      如果过了循环还没有返回,说明链表中不存在与这个 object 等同的数据,那么返回 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 线性表越界, 当前线性表共有 %@ 个元素",  __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 线性表越界, 当前线性表共有 %@ 个元素",  __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
  
  /// 插入操作和链表几乎一致,需要注意的是,有判断是否要更新 尾结点(self.rear),因为用户有可能在表尾插入
  /*  
      有的同学可能会担心如果 prior 是尾结点了,那么循环怎么处理呢?不用担心,看下我的分析
      如果 prior 是尾结点,那么 node.next = prior.next,这个时候新插入的结点的指针域 next 指向 prior 的 next 也就是头结点
      然后我们这样 prior.next = node ,打破了原来尾结点的环,让尾结点的指针域指向新的结点,构成循环
  */
  
  if (prior == self.rear) { /// 更新 rear 指针
    self.rear = node;
  }
}

- (id)removeObjectAtIndex:(NSUInteger)index
{
  ///  删除和插入差不多,我就不多说了,相信好好看的同学,已经看懂了
  NSAssert(index < self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素",  __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; /// 要删除的节点
  prior.next = node.next;
  node.next = nil;
  
  self.count--;
  
  if (node == self.rear) { /// 更新尾指针
    self.rear = prior;
  }
  
  return node.data;
}

- (BOOL)containsObject:(id)object
{
  ///  这个也就不说了吧,遍历整张表,如果结点的数据域 data 等同于 object ,理解返回 YES;如果循环结束,没有返回,那么说明不包含 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
{
  ///  找到头结点 head
  id <CHRSinglyLinkedListNodeProtocol> head = self.rear.next;
  
  /*
      我这边是这样的思路
  
      一直删除第一个结点, 也就是 head.next(就算 head.next 是尾结点,那么 head.next = node.next , 也就变成了空链表的情况,如图 2 )
      然后除了循环之后, 更新 rear 和 count
  */
  while (head != head.next) {
    id <CHRSinglyLinkedListNodeProtocol> node = head.next; /// 要删除的节点
    head.next = node.next;
    node.next = nil;
  }
  self.rear = head;
  self.count = 0;
}

@end

End

大概的代码就是这些,相信思路大家已经有了大致了解,我觉得只要整整理解了空表的情况, 也就是 rear == head == read.next ,那么就应该很好理解了。我在看书的时候,看懵逼了好多遍, 实现了三天,才真正理解了,唉,我这麻瓜。

对了,剧透一下,现在我们实现了 链表、循环链表, 但是,我们实现的是单(向)链表。既然有单向链表,那么就有双向链表咯,其实很简单,双向链表就是加一个指针域 prior ,使用起来更加灵活,某些时候可以达到更高的效率,我的理解就是用空间来换时间。双链表也可以循环,其实道理很类似,我们下一篇再说。

还有,这篇昨天写了一半,今天上午写了一半,可能有一些思路断掉了,看不懂,或者有错误的话,大家请指正。本文的代码,还是放在那个仓库中,欢迎大家测试,找 bug ,尤其是内存问题。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容