前言
在之前的文章中, 大家还记得我的链表和结点、结点协议的名字么?
1.CHRSinglyLinkedListNodeProtocol
2.CHRSinglyLinkedListNode
3.CHRSinglyLinkedList
4.CHRSinglyCircularLinkedList
细心的朋友应该还记得我说的 Singly ,配合 LinkedList ,翻译过来就是 单向链表结点协议、单向链表结点、单向链表和单向循环链表 了。为什么说是单向的呢? 很简单,因为我们的链表是 **head -> 0 -> 1 -> ... -> n ** 的,每一个结点只知道下一个结点 next 。
如果说我已经知道了一个结点 m (0 <= m <= n),我想找到 m - 1 的结点呢? 那么我们就要遍历一遍链表,用之前的指针,找到 m 的 prior 结点。 WTF ,这也太不灵活了,如果我的链表有 1000 W 个结点,m = 999,9999那遍历一次成本也太高了啊,要执行 m - 1 次。有没有灵活一点的办法呢?
请看今天的走进科学,啊呸, 双向链表 吧。(也叫双链表)
双链表
定义
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
我们来看图吧,别被这定义吓到了,见图 1 。大家注意,这里的一条直线是指逻辑上的,内存中的顺序无所谓,也就是理解 链 的思想。
首先把虚线部分去掉来看,和单链表一样,就是多了一个前驱(prior) ,指向每一个结点的前一个结点。头结点也有 prior 结点,只是 prior 为 nil, 就像结点 5 也有后继结点 next , next 为 nil 一样,避免图太长,懒着画了。
Q:结点 4 的前驱结点 prior 和 后继结点 next 分别是?
**A:结点 3 、结点5 **
Q:结点 0 的前驱结点 prior 和 后继结点 next 分别是?
**A:结点 head 、结点 1 **
Q:结点 5 的前驱结点 prior 和 后继结点 next 分别是?
**A:结点 4 、nil **
Q:头结点的前驱结点 prior 和 后继结点 next 分别是?
**A:nil 、结点 0 **
相信看完了上面一连串的 QA 之后,大家已经明白了,所有的结点相同对待这个道理,也许在单链表的章节就懂了,恕我啰嗦。
接下来我们分析下各种情况:
1. 双链表空表
分析:
双链表空表的时候,和单链表几乎一样,就只有一个头结点, 头结点的指针域 next 、prior 都为 nil 。
2. 向双链表中插入一条数据
我们来看图说话。
分析:
先来看图 2 ,头结点的前驱 prior 为 nil, 就没有画出来,后继 next 也为 nil ,和单链表差不多,在插入前,先构造一个新的结点,把数据包装到数据域 data 。也就是图中的结点 0 做的事情, 构造一个结点, 把 0 塞进去,结点 0 的前驱 piror 和后继 next 都为 nil 。
然后我们来看图 3 ,插入到链表后, 头结点的 next 指向 结点 0 ,结点 0 的前驱指向头结点。
Q:那么如果是中间结点和尾结点呢?
尾结点和空表插入的操作不是一毛一样么?大家想想看。
中间结点还有处理一个问题,就是处理新插入结点的后继,把链连接好。
例如:向图 3 中 0 位置,插入数据 x 。
- 我们先构造一个结点, 把 x 包装到结点中,我们叫这个结点为结点 X ;
- 结点 X 的前驱指向头结点;
- 结点 X 的后继指向头结点的后继(结点 0 );
- 头结点的后继结点(结点 0)的前驱指向结点 X;
- 头结点的后继指向 X。
其实 步骤3 和步骤 4 没什么,主要是步骤 1 和步骤 2的顺序,和单链表类似,如果先做了步骤 2 ,那么结点 0 后面的链就挂了。
那么如果是表尾插入呢?其实是一样的,用 Objective-C 来解释就是,如果插入的是表为,也就是图 4 中的结点 0 为 nil,那么结点的数据域和指针域都为 nil,对 nil 的操作的安全的。
3. 从双链表中删除一条数据
分析:
插入理解了,删除就更好理解了,我就不多说了,扔张图,见图 5。
如果说要删除结点 2 的话,这个感觉就简单很多了:
- 让结点 1 的后继指向结点 3;
- 结点 3 的前驱指向结点 1;
- 干掉结点 2 。
删除第一个结点和删除最后一个结点,头结点和尾结点可以一样处理,和插入类似, 对 nil 操作是安全的,逻辑上 nil 是链表的结束。
代码实现
说了这么多,来看看代码实现吧,一点都不复杂...
双链表结点协议
#import
@protocol CHRDoubleLinkedListNodeProtocol
@property (nonatomic, weak, readwrite, nullable) id prior; /// next 来保留引用就好了, prior 就直接使用 weak 了
@property (nonatomic, strong, readwrite, nullable) id next;
@property (nonatomic, strong, readwrite, nullable) id data;
@end
双链表结点 header
#import "CHRDoubleLinkedListNodeProtocol.h"
@interface CHRDoubleLinkedListNode : NSObject
@end
双链表结点 implementation
#import "CHRDoubleLinkedListNode.h"
@interface CHRDoubleLinkedListNode ()
{
id _next;
id __weak _prior;
id data;
}
@end
@implementation CHRDoubleLinkedListNode
@synthesize next = _next;
@synthesize prior = _prior;
@synthesize data = _data;
@end
双链表 header
#import "CHRLinearList.h"
@interface CHRDoubleLinkedList : CHRLinearList
@end
双链表 implementation
#import "CHRDoubleLinkedList.h"
#import "CHRDoubleLinkedListNode.h"
@interface CHRDoubleLinkedList ()
{
id _next;
id __weak _prior;
id data;
}
@property (nonatomic, assign) NSUInteger count;
@end
@implementation CHRDoubleLinkedList
@synthesize next = _next;
@synthesize prior = _prior;
@synthesize data = _data;
@synthesize count = _count;
- (instancetype)initWithObjects:(id)objects, ...
{
self = [super init];
if (self) {
_count = 0;
id prior = self; /// self 是 头结点
id object = objects;
va_list params;
va_start(params, objects);
while (object) {
// 构造新的结点,并将数据 object 包装到 node 中
id node = [[CHRDoubleLinkedListNode alloc] init];
node.data = object;
prior.next = node;
node.prior = prior;
prior = node; /// 更新 prior
_count++; /// 更新 count
object = va_arg(params, id); /// 更新 object
}
va_end(params);
}
return self;
}
- (BOOL)isEmpty
{
return !self.next;
}
- (id)objectAtIndex:(NSUInteger)index
{
/// 断言 index 没有越界
/// self.count 不向单链表中,是遍历得到,耗费时间。所以,这里直接用 self.count 断言
NSAssert(index < self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id node = self.next;
while (ctrIndex < index) {
node = node.next; /// 更新 node
ctrIndex++; /// 自增 ctrIndex
}
return node.data;
}
- (NSUInteger)indexOfObject:(id)object
{
/// 与单链表相同了,只需要一个 后继指针 next 就可以搞定
NSUInteger index = 0;
id node = self.next;
while (node) {
if ([node.data isEqual:object]) {
return index;
}
node = node.next;
index++;
}
return NSNotFound;
}
- (void)insertObject:(id)object atIndex:(NSUInteger)index
{
/// 断言 object 非空
NSAssert(object, @"%s, %d, 向线性表中插入了 nil 是不允许的", __FILE__, __LINE__);
/// self.count 不向单链表中,是遍历得到,耗费时间。所以,这里直接用 self.count 断言
NSAssert(index <= self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id prior = self;
while (ctrIndex < index) {
prior = prior.next;
ctrIndex++;
}
CHRDoubleLinkedListNode *node = [[CHRDoubleLinkedListNode alloc] init];
node.data = object;
node.prior = prior;
node.next = prior.next;
prior.next.prior = node;
prior.next = node;
self.count++; /// 更新 count
}
- (id)removeObjectAtIndex:(NSUInteger)index
{
/*
这里的 node 是要删除的结点
而单链表中是要删除结点的前驱
因为双向,所以可以很容易的找到自己的前驱,前驱的前驱、后继、后继的后继 and so on...
这里直接找自己就好了
*/
NSAssert(index < self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id node = self.next;
while (ctrIndex < index) {
node = node.next; /// 更新 node
ctrIndex++; /// 自增 ctrIndex
}
node.prior.next = node.next;
node.next.prior = node.prior;
node.next = nil; /// 把 node.next 置空
node.prior = nil; // 因为 prior 是 weak, 所以,理论上是不用管的,不过为了安全,万一以后有人改你的代码呢,一个不注意可能出问题,把感觉潜在的风险扼杀在摇篮中吧
self.count--; /// 更新 count
return node.data;
}
- (BOOL)containsObject:(id)object
{
/// 和单链表一样
id node = self.next;
while (node) {
if ([node.data isEqual:object]) {
return YES;
}
node = node.next;
}
return NO;
}
- (void)removeAllObjects
{
id head = self;
@autoreleasepool {
while (head.next) {
id node = head.next; /// 要删除的结点(一直删除第一个结点)
node.prior.next = node.next;
node.next.prior = node.prior;
node.prior = nil;
node.next = nil;
}
}
self.count = 0;
}
@end
循环双链表
其实双链表并不是很常用,我们一般写双链表一般写循环双链表,其实就是在形成个闭环后,处理下循环引用。
双链表成了环的话,那么我们可以很容易的通过头结点的前驱 prior 找到尾结点,所以时间复杂度就是 O(1) 了,空间换算时间,确实很容易想,也好实现 。
我们来看下示意图,如图 6(用 Numbers 画图真难受...)。其实就是在图 1 的基础上加了一根实线,一根虚线,让结点 5 的 next 指向头结点;头结点的 prior 指向结点 5 。
我们来分析下各种情况吧:
1. 空表
如图 7 所示,头结点的 next 指向头结点自己,头结点的 prior 也指向自己。
2. 向双循环链表插入数据
其实插入数据和双向链表操作一样,不需要像用尾结点表示的单循环链表,要更新 rear。这里图就不画了,大家多动脑。
3. 从双循环链表删除数据
删除和插入类似,和双向链表删除数据操作相同,同理不说了(大家自己一定考虑清楚,尤其是边界值)。
代码实现
#import "CHRDoubleCircularLinkedList.h"
#import "CHRDoubleLinkedListNode.h"
@interface CHRDoubleCircularLinkedList ()
@property (nonatomic, strong) CHRDoubleLinkedListNode *head;
@property (nonatomic, assign) NSUInteger count;
@end
@implementation CHRDoubleCircularLinkedList
@synthesize count = _count;
- (void)dealloc
{
/// 打破保留环,方式和单循环链表相同
_head.prior.next = nil;
}
- (instancetype)initWithObjects:(id)objects, ...
{
self = [super init];
if (self) {
_count = 0;
_head = [[CHRDoubleLinkedListNode alloc] init];
id prior = _head; /// self.head 是 头结点
id object = objects;
va_list params;
va_start(params, objects);
while (object) {
// 构造新的结点,并将数据 object 包装到 node 中
id node = [[CHRDoubleLinkedListNode alloc] init];
node.data = object;
prior.next = node;
node.prior = prior;
prior = node; /// 更新 prior
_count++; /// 更新 count
object = va_arg(params, id); /// 更新 object
}
va_end(params);
/// 上面操作和双链表一样, 我们要使双链表循环, 升级为双向循环链表
prior.next = _head; /// prior 到这里已经是尾结点了,就算是空表,尾结点就是 self.head,头尾重合咯
_head.prior = prior;
}
return self;
}
- (BOOL)isEmpty
{
/*
空链表的情况, 前驱(prior) 和后继 (next) 都指向头结点自己 (self.head)
维基百科上是这样写的:
return self.head.prior == self && self.head.next == self.head
这里用一个指针就可以判断,所以我就没有 && self.head.prior == self.head 了
*/
return self.head.next == self.head;
}
- (id)objectAtIndex:(NSUInteger)index
{
/// 和双链表操作一样
NSAssert(index < self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id node = self.head.next;
while (ctrIndex < index) {
node = node.next;
ctrIndex++;
}
return node.data;
}
- (NSUInteger)indexOfObject:(id)object
{
/// 与单链表相同了,只需要一个 后继指针 next 就可以搞定
NSUInteger index = 0;
id node = self.head.next;
while (node != self.head) {
if ([node.data isEqual:object]) {
return index;
}
node = node.next;
index++;
}
return NSNotFound;
}
- (void)insertObject:(id)object atIndex:(NSUInteger)index
{
/// 插入同双链表
NSAssert(object, @"%s, %d, 向线性表中插入了 nil 是不允许的", __FILE__, __LINE__);
NSAssert(index <= self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id prior = self.head;
while (ctrIndex < index) {
prior = prior.next;
ctrIndex++;
}
CHRDoubleLinkedListNode *node = [[CHRDoubleLinkedListNode alloc] init];
node.data = object;
node.prior = prior;
node.next = prior.next;
prior.next.prior = node;
prior.next = node;
self.count++;
}
- (id)removeObjectAtIndex:(NSUInteger)index
{
/// 删除同双链表
NSAssert(index < self.count, @"%s, %d 线性表越界, 当前线性表共有 %@ 个元素", __FILE__, __LINE__, @(self.count));
NSUInteger ctrIndex = 0;
id node = self.head.next;
while (ctrIndex < index) {
node = node.next;
ctrIndex++;
}
node.prior.next = node.next;
node.next.prior = node.prior;
node.next = nil;
node.prior = nil;
self.count--;
return node.data;
}
- (BOOL)containsObject:(id)object
{
/// 和单链表一样
id node = self.head.next;
while (node != self.head) {
if ([node.data isEqual:object]) {
return YES;
}
node = node.next;
}
return NO;
}
- (void)removeAllObjects
{
/// 同双链表
id head = self.head;
@autoreleasepool {
while (head.next != self.head) {
id node = head.next;
node.prior.next = node.next;
node.next.prior = node.prior;
node.prior = nil;
node.next = nil;
}
}
self.count = 0;
}
@end
End
链表是基础中的基础,希望大家基础扎实。因为:
堆栈是用链表实现的、队列是用链表实现的
队列是用链表实现的、队列是用链表实现的
树,通过一定的变形用二叉树表示。二叉树可以总得来说,和链表也脱不开干系..
写的好累,放假要好好休息休息,尽可能的把 堆栈 和 队列 写完。
链的思想非常重要。比如说 iOS 中的响应链、树形结构中的每一条链、栈(导航控制器)、队列(比如 operation) and so on...
大家别忘记初中哈,我认为思想最重要,语言无所谓。
Im Chris. zZ~