[一周一算法] 链表与插入排序

Posted 糖炒小虾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[一周一算法] 链表与插入排序相关的知识,希望对你有一定的参考价值。

前言

链表是最常见的数据结构之一。我们在后面介绍的树、图都会使用链表来记录。例如二叉树、邻接表。在对栈空间有限制的低端设备上(十年前的设备),使用链表可以调用堆空间,进而开启更大的内存空间。

我认为对于学好数据结构和算法帮助最大的事情就是画图,画图去推各种情况。所以本文这里也会用各种灵魂画风来做示意图。所有地方均选用单向链表来做演示,双向链表留给大家自己做练习。我这里使用的各种代码实现,全都会选择用最直白的方式,甚至是效率相对低下的代码,尽量去掉面向对象或者面向协议还有泛型编程的思想,以助于大家理解。

链表


如果写成代码的话,那么它应该是这样子的。但是注意一个问题,在 C 中,我们会选择 Struct ,而 Swift 的 Struct 都是 Copy,无法达到链表的效果,所以只能用 Class。上文也说了,所以这里不使用泛型。

class Link {    
   var value: Int    var next: Link?    
   init(_ val: Int) {        value = val    } }

链表的遍历与随机查找

因为链表每一单元只有他的父节点知道,所以我们无论是遍历整个队列,还是想随机查找某一个单元,实现全都是一样的:从父节点 One By One 的向后找,直到 Next 为空。

对于遍历和查找,我们可以给刚才的类,提供两个方法:查找元素打印整个链表

打印整个列表对我们后面做的增删改查是非常必要的,所以姑且叫它 debug 方法吧。这里有个细节,并不是链表的,而是 Swift 的。

Swift 2.0 之后 print 相当于 println。如果你不想它在后面添加 \n 的话,我们需要把 terminator 参数置为空字符串

func debug() {  
   var t = self    while true {    
       if let nxt = t.next  {      
           print("\(t.value) -> ",terminator: "")            t = nxt        } else {      
           print(t.value)      
           break        }    } }

下面是展示用的 demo, 注意一下,本文的演示部分代码是相关联的。所有的代码可以拼入一个 PlayGround,说实在的 Playground 很适合写算法类的东西。

let a = Link(1)
let b = Link(3)
let c = Link(4) a.next = b b.next = c

var head = a head.debug()
//输出:1 -> 3 -> 4

查找和这个相类似,只是在需要的时候 break 掉即可,注意找不到的时候要返回空,所以函数返回值,应该是个 Link? 类型。

func find(val: Int) -> Link? {    
   var t = self    while true {        
       if t.value == val {            
           return t        }        
       
       if let nxt = t.next {            t = nxt        } else {            
           return nil        }    } }

在 C 语言中,循环一个链表有更优雅的写法,然而 Swift 3.0 即将移除这个特性,以及貌似写出来会有叹号,所以我这里就用 C 写一下:

for (Link* t = head; t->next != NULL; t = t->next) {  //dosth  }

相信大家都会或多或少的背过一些时间复杂度,例如:遍历一个数组是 O(N) 数组的随机查找是 O(1),链表全都是 O(N)。现在知道这个 O(N) 是怎么出来的了吧。

添加元素

添加和删除都需要考虑三种情况:链表头链表中间链表尾部

对于三种情况,来了解的方法肯定还是画图!于是我们熟悉的灵魂画风又来了。

常规插入是这样的(黑线是原始的,蓝线为修改):
[一周一算法] 链表与插入排序

  1. 若我们要把 c 节点插入 ab 节点之间。

  2. c 的 next 指向 b

  3. a 的 Next 指向 c

由此可见,对于插入链表尾部的情况。是同理的,毕竟 b 为空即为所求。
代码如下:

let d = Link(2)

// 中间插入
d.next = b a.next = d head.debug()

// 行尾插入

let f = Link(5)
var t = head
// 找到行尾

while true {    
   if let nxt = t.next {        t = nxt    } else {        
       break    } } t.next = f head.debug()

// 结果 1 -> 2 -> 3 -> 4 -> 5

而头部插入是与众不同的,毕竟修改了 head 指针。

  1. 若我们要在 head 前插入 x

  2. x 的 next 指向 head

  3. head 指向 x

// 行首插入

let e = Link(0) e.next = head head = e head.debug()
// 结果 0 -> 1 -> 2 -> 3 -> 4 -> 5

删除

删除与插入非常类似,对于普通删除的话,就是把某节点的 next 指向 next?.next 即可。而删除行首的话,只需要把 head 指针向后移动即可。

灵魂画风如下:

// 行首删除

if let nxt = head.next {    head = nxt } head.debug()

// 行中删除 删除第4个
t = head

// 删除需要知道父亲是谁,那就是第三个。所以需要移动两次指针
for i in 0..<2 {    
   if let nxt = t.next {        t = nxt    } } t.next = t.next?.next head.debug()

以及,对于 Swift 来说,删除是相对简单的。因为不需要用内存管理 (笑。如果要是用 C 的话,删除的节点的要 free 掉。

哨兵

我们会发现,链表操作非常繁琐的一点是要判断 Head 这件事。那么我们为啥不定义一个 Header 呢?只负责告诉我们真正的链表头是他的 next。这样所有的操作都统一了。

具体的实现,我们把它放在插入排序里面说。

插入排序

插入排序基本上是给链表量身定制的低级排序。所谓低级排序,就是效率非常低下的排序。它的核心思想就是,一群人按照身高排队的做法。

  1. 先随便找个排头

  2. 之后每个人从头到尾开始比,找到合适的位置就站进去。

为什么说是量身定做,因为对于链表来说,塞入一个元素的成本是那么的低,而数组需要涉及到移动以及分配新的内存。

// 用 badfood 这个 magic number 当哨兵
// 其实当约定好第一个数据是哨兵的时候,用 0 也是没问题的。

head = Link(0xbadf00d) for _ in 0..<100 {    
   let a = Link(random() % 1000)    
   var t = head    
   while true {        
       if let next = t.next {            
           if next.value > a.value {                a.next = next              
               t.next = a                
               break            }            t = next        } else {            t.next = a            
           break        }    } } head.debug()

通过源码可以发现,如果说我提供的数据是递增的。那么循环次数就是 1+2+3+4+…+n 次,也就是 N^2 的时间复杂度,所以我们常说插入排序的时间复杂度是 O(N^2) 就是这么算出来的。

仿佛和队尾删除有关的代码非常啰嗦,主要是我想多强化几次遍历的使用。正确做法应该是用双向链表和 tail 指针来维护它,因为不用双向链表的话,队尾的删除是没法做的,否则还要再维护一个指针,队尾的父亲。


不知道我的讲解对大家的帮助有多大呢?欢迎与我讨论。


戳原文链接,可以在 Leetcode 练习一下链表的基本操作。

勘误

- 上一篇基础知识中,有一个 O(ijk) 是脑抽写错了,应该是 O(N^3)
- 有个细节忘了说,对于大 O 表达法,一般情况下常数我们也是不考虑的。比如 O(2N^2),我们会省略掉 2 只关注 N^2 这件事。

以上是关于[一周一算法] 链表与插入排序的主要内容,如果未能解决你的问题,请参考以下文章

跳表与二分查找

算法题 14 LeetCode 147 链表的插入排序

表插入排序算法

C++算法之链表排序的代码

数据结构算法 LinkList (Insertion Sort List 链表插入排序)

数据结构排序算法