链表2 - 写链表代码的技巧

0x00 前言

专栏这一讲主要讲解写链表代码的一些技巧,就我的经验来说,链表代码写起来很麻烦,主要是链表的指针指来指去就会变得很混乱,还有就是边界问题的处理,使得链表的处理很麻烦。事实上任何问题的边界问题值得注意。

0x01 技巧

1、理解指针或引用的含义

java中并没有指针,可以理解为引用,常见的有两种

p = q.next
p.next = q

第一句是给p赋值,将p指向q的下一结点
第二句是p下一结点设置为q

2、利用哨兵简化实现难度

在处理边界问题的时候,往往需要针对null进行判断,为了避免每次在循环中判断一次,我们可以在边界就设置好满足的条件,例如:在数组中查找元素key

find(int[] a,int n,int key){
int i = 0
    while (i<n){
        if (a[i] == key){
            return i
        }
        ++i
    }
}
return -1

以上每一次循环都要判断i<n,我们可以在数组末尾处设置哨兵,这样保证数组一定不会越界,然后判断一下就行了

if(a[n-1]==key){
    return n-1
}
tmp = a[n-1]
a[n-1]=key
while (a[i]!=key){
    ++i
}
if (i==n-1){
    return -1
}else {
    reutrn i
}

以上代码在数组末尾设置了一个哨兵,这样相比第一份代码每次循环少判断一次i<n,对于大数据量的情况下,这个累积的效率就很明显了

在链表中的哨兵,经常是设置一个头哨兵,不存储数据,指向链表,可以避免null的检测

3、边界条件处理

常见的边界问题,需要注意一下几种情形

  • 如果链表为空,代码能都正常工作
  • 如果链表只包含一个结点时
  • 如果链表只包含两个结点时
  • 代码逻辑在处理头结点和尾结点时

0xff 总结

除了上述几种技巧外,还有画图辅助等方法帮助思考,并且以上几种技巧不仅仅适用于链表,其他数据结构算法的题也都适用,重要的时多些多练,对于链表,有一下几种常见操作需要掌握

  • 单链表反转
def reverse(node: Option[Node]): Option[Node] = {
    if (node.isEmpty){
      return node
    }
    var pre:Option[Node] = None
    var current:Option[Node] = node
    while (current.get.next.nonEmpty){
      var tmp:Option[Node] = current.get.next
      current.get.next = pre
      pre = current
      current = tmp
    }
    current.get.next = pre
    current
}
  • 链表中环的检测

环的检测使用两个快慢指针完成


image

如图,每次慢指针走一步,快指针走两步,如果有环,那么快慢指针一定会相遇
假设:

起点距离环入口距离为m
环的入口点距离相遇点为k
环长度为n
相遇时,慢指针绕环a圈,快指针绕环b圈
所以,当相遇时,慢指针走了 m+an+k,快指针走了 m+bn+k
根据两个指针的速度,得到 2(m+an+k) = m+bn+k => m+k = (b-2a)n
随便找一组数据都能满足上述等式

def ifCycle(node:Option[Node]):Boolean={
    var slow,fast = node
    while (fast.isDefined && fast.get.next.isDefined){
      slow = slow.get.next
      fast = fast.get.next.get.next
      if (slow==fast){
        return true
      }
    }
    false
 }

我们还可以求环的入口点,当相遇完成后,将慢指针移动到起点,快指针在相遇点,然后快指针和慢指针每次都运动一步,下一次相遇点一定是环的入口点
证明:

当慢指针运动到入口点时,快指针也运动了m步,因为m+k是n的整数倍,所以此时快指针一定距离启动也是a的距离,即入口点

// 不能返回循环的链表,会报StackOverflowError,这里返回入口链表的值,假设链表中的数据都不一样,而且都不是0
def findEntrance(node:Option[Node]): Int ={
    var slow,fast = node
    // cycle记录是否有环 flag记录是否停止循环
    var cycle,flag = false
    while (!flag){
      if (fast.isDefined && fast.get.next.isDefined){
        slow = slow.get.next
        fast = fast.get.next.get.next
        if (slow==fast){
          cycle = true
          flag = true
        }
      }else{
        flag = true
      }
    }
    if (cycle){
      slow = node
      while (slow!=fast){
        slow = slow.get.next
        fast = fast.get.next
      }
      slow.get.data
    }else{
      // 没有环返回0
      0
    }
}

参考资料:链表环检测算法

  • 两个有序链表合并
def merge(node1: Option[Node],node2:Option[Node]): Option[Node] ={
    if (node1.isEmpty){
      node2
    }
    if (node2.isEmpty){
      node1
    }
    // small永远指向small和big之间较小的,然后循环比较small的下一个结点和big结点
    var big = node1
    var small = node2
    if (big.get.data < small.get.data){
      small = node1
      big = node2
    }
    // 记录较小的头结点,返回结果
    val head = small
    // 只要small的下一个结点和big不为空,就可以比较
    while (small.get.next.isDefined && big.isDefined){
      if (small.get.next.get.data < big.get.data){
        small = small.get.next
      }else{
        var smallNext = small.get.next
        var bigNext = big.get.next
        small.get.next = big
        big.get.next = smallNext
        // 调整之后 big、smallNext、bigNext中big最小,bigNext最大
        small = big
        big = bigNext
      }
    }
    // 跳出循环有两种情况,1种是small指向了最后一个结点,此时把big接到small的后面就行了
    // 还有一种是big指向null了,此时不用变化,head直接返回就行了
    if (big.isDefined){
      small.get.next = big
    }
    head
  }
  • 删除链表倒数第n个结点
// 快慢指针,快指针先走n步,然后快慢指针同时走直到快指针到头
def deleteNode2(node:Option[Node],n:Int): Option[Node] = {
    require(n>0,"n必须大于0")
    var slow,fast = node
    var index = 0
    while (fast.isDefined && index<n){
      index += 1
      fast = fast.get.next
    }
    require(index==n,"至少包含n个结点")
    // 删除头结点
    if (fast.isEmpty){
      return node.get.next
    }
    while (fast.isDefined && fast.get.next.isDefined){
      slow = slow.get.next
      fast = fast.get.next
    }
    slow.get.next = slow.get.next.get.next
    node
 }
// 先反转链表在删除第k个结点,然后在反转链表
def deleteNode(node:Option[Node],n:Int): Option[Node] ={
    var head = ReverseLinked.reverse(node)
    var cur = head
    // 删除头结点
    if (n==1){
      head = head.get.next
      cur.get.next = None
      return ReverseLinked.reverse(head)
    }
    // cur指向删除结点的上一个结点
    for (i <- 1 until n-1){
      cur = cur.get.next
    }
    val tmp = cur.get.next
    cur.get.next = tmp.get.next
    tmp.get.next = None

    ReverseLinked.reverse(head)
 }
  • 求链表的中间结点
// 返回中间结点,如果是偶数个结点,返回中间靠右的结点
def findMidNode(node:Option[Node]): Option[Node] ={
    var slow,fast = node
    while (fast.isDefined && fast.get.next.isDefined){
      slow = slow.get.next
      fast = fast.get.next.get.next
    }
    slow
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 转载请注明出处:http://www.jianshu.com/p/c65d9d753c31 在上一篇博客《数据结构...
    Alent阅读 3,553评论 4 74
  • 链表(下):如何轻松写出正确的链表代码? 上一节我讲了链表相关的基础知识。学完之后,我看到有人留言说,基础知识我都...
    GhostintheCode阅读 1,326评论 2 3
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,479评论 0 25
  • 数组中重复的数字(一维数组) 问题:在一个长度为长度为n的数组里的所有数字都在0-n-1之间。找出任意一个重复的数...
    Drama_Du阅读 1,171评论 0 0
  • 前女友多的好处是,没事可以翻翻微博,内心还是贱的。虽然是不打搅,只是一种窥探欲望。 今天看到国家出的农业新政,感觉...
    琛周阅读 130评论 0 0