假设以带头结点的循环链表表示队列_08.链表(实战篇)

引入开篇问题:

如何轻松写出正确的链表代码?带着这个问题,开始今天的学习吧!

几个写链表的代码技巧

技巧一:理解指针或引用的含义

C语言有"指针"的概念;Java,Python的"引用"相当于C语言的"指针"。

"指针"和"引用"都是存储所指对象的内存地址。

实际上我们对于指针的理解可以用下面这几句话就可以了:

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

链表常用的表示法:

p ->next = q 表示p结点中的next中的next指针存储了q结点的内存地址。

p -> next = p -> next -> next 表示p结点的next指针存储了p的下下一个结点的内存地址4

技巧二:警惕指针丢失和内存泄漏

不知你们是不是和我一样在写链表代码时候,指针指来指去,一会就不知到指到哪去了。所以在写指针代码时候一定要注意不要带去了指针

你们肯定会问指针是怎么丢弃的?拿单链表的插入操作为例来分析一下吧!

7b39eed3300439c95d2b570074602a11.png

如图所示,我们希望在结点a和相邻的结点b之间插入结点x,假设指针p指向结点a。如果将代码写成下面这个样子,就会发生指针丢失和内存泄漏

p -> next = x;  // 将 p 的next指针指向 x 结点
x -> next = p -> next;  // 将 x 的结点的 next指针指向b结点

初学者经常会在这儿犯错。p->next 指针在完成第一步操作之后,已经不再指向结点 b 了,而是指向结点 x。第 2 行代码相当于将 x 赋值给 x->next,自己指向自己。因此,整个链表也就断成了两半,从结点 b 往后的所有结点都无法访问到了。

对于有些语言来说,比如C语言,内存管理由程序员负责,如果没有手动释放结点对应的内存空间,就会产生内存泄露。所以,在插入结点时,一定要注意操作的顺序,要先将结点 x 的 next 指针指向结点 b,再把结点a的next指针指向结点x,这样就不会丢失指针了,导致内存泄漏。所以,对于刚刚的插入代码,我们只需要把第一行和第二行代码进行颠倒一下就可以了。

x - > next = p - > next;
p - > next = x;

同理,删除链表结点时,也一定记得手动释放内存空间,否则也会出现内存泄漏问题。当然,对于Java这样虚拟机自动管理内存的编程语言来说,这不需要考虑这个问题了。

技巧三:利用哨兵简化实现难度

对于单链表的插入和删除操作,如果我们在p后面插入一个新的结点,下面两行代码搞定

new_node -> next = p -> next;
p - > next = new_node;

但是,当我们要向一个空链表中插入第一个结点,刚刚的逻辑就不能用了。我们需要进行下面这些特殊处理,其中head表示链表的头结点。所以,从这段代码,可以看出,对于单链表的插入操作,第一个结点和其他结点的插入逻辑是不一样的。

if (head == Null){
    head = new_node;
}

再来看单链表结点删除操作。如果要删除结点p的后继结点我们只需要一行代码就可以搞定。

p -> next = p -> next -> next

但是,如果我们要删除链表中的最后一个结点,前面的删除代码就不能工作了。跟插入类似,我们也需要对于这种情况特殊处理。写成以下的代码:

if (head - > next == null) {
    head = null;
}

针对链表的插入,删除操作,需要对插入第一个结点和最后一个结点的情况进行特殊处理。

head表示头结点指针指向链表中的第一个结点 head = null 表示链表没有结点

哨兵是为了解决“边界问题”的,不直接参于业务逻辑。如果我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫做不带头链表。

带头链表:

哨兵结点是不存储任何数据的。因为哨兵结点一直存在着,所以插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑。

5ed30d81b5c81eb84d01c0f2ae517648.png

实际上,这种利用哨兵简化编程难度的技巧,在插入排序,归并排序,动态规划等都可以用到

下面利用C语言举个例子,不涉及高级语法

示例一:

// 在数组a中,查找key,返回key所在的位置
// 其中,n表示数组a的长度
int find(char* a, int n, char key) {
  // 边界条件处理,如果a为空,或者n<=0,说明数组中没有数据,就不用while循环比较了
  if(a == null || n <= 0) {
    return -1;
  }
  
  int i = 0;
  // 这里有两个比较操作:i<n和a[i]==key.
  while (i < n) {
    if (a[i] == key) {
      return i;
    }
    ++i;
  }
  
  return -1;
}

示例二:

// 在数组a中,查找key,返回key所在的位置
// 其中,n表示数组a的长度
// 我举2个例子,你可以拿例子走一下代码
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 7
// a = {4, 2, 3, 5, 9, 6}  n=6 key = 6
int find(char* a, int n, char key) {
  if(a == null || n <= 0) {
    return -1;
  }
  
  // 这里因为要将a[n-1]的值替换成key,所以要特殊处理这个值
  if (a[n-1] == key) {
    return n-1;
  }
  
  // 把a[n-1]的值临时保存在变量tmp中,以便之后恢复。tmp=6。
  // 之所以这样做的目的是:希望find()代码不要改变a数组中的内容
  char tmp = a[n-1];
  // 把key的值放到a[n-1]中,此时a = {4, 2, 3, 5, 9, 7}
  a[n-1] = key;
  
  int i = 0;
  // while 循环比起代码一,少了i<n这个比较操作
  while (a[i] != key) {
    ++i;
  }
  
  // 恢复a[n-1]原来的值,此时a= {4, 2, 3, 5, 9, 6}
  a[n-1] = tmp;
  
  if (i == n-1) {
    // 如果i == n-1说明,在0...n-2之间都没有key,所以返回-1
    return -1;
  } else {
    // 否则,返回i,就是等于key值的元素的下标
    return i;
  }
}

这两段代码相信很容易看懂吧,通过比较这两个示例代码,如果当a字符串很长时,达到几万,甚至几十万,是那段代码的运行效果高?答案肯定是示例二。两端代码中执行次数次数最多就是while循环那部分。第二段代码中,通过一个哨兵a[n-1] = key ,成功省掉了一个比较语句i < n,在字符串a很长的时候,累积的节省时间就很明显了。

利用哨兵简化编码

哨兵可以理解为它可以减少特殊情况的判断,比如判空,比如判越界,比如减少链表插入删除中对空链表的判断,比如例子中对i越界的判断。

哨兵的巧妙就是提前将这种情况去除,比如给一个哨兵结点,以及将key赋值给数组末元素,让数组遍历不用判断越界也可以因为相等停下来。

使用哨兵的指导思想应该是将小概率需要的判断先提前扼杀,比如提前给他一个值让他不为null,或者提前预设值,或者多态的时候提前给个空实现,然后在每一次操作中不必再判断以增加效率。

技巧四:重点留意边界条件处理

软件开发中,代码在一些边界或者异常情况下,最容易产生 Bug。要实现没有 Bug 的链表代码,一定要检查边界条件是否考虑全面,以及代码在边界条件下是否能正确运行。

常用检查链表代码是否正确的边界条件有:

  1. 如果链表为空时,代码是否能正常工作?
  2. 如果链表只包含一个结点,代码是否能正常工作?
  3. 如果链表只包含两个结点时,代码是否能正常工作?
  4. 代码逻辑在处理头结点和尾节点时候,代码是否能正常工作?

如果这些边界条件下都没有问题,那基本不也就没有问题了。

当然,边界条件不止针对不同的场景,可能还有特定的边界条件。

写任何代码时,一定要多想想,你的代码在运行的时候,可能会遇到哪些边界情况或者异常情况。遇到了应该如何应对,这样写出来的代码才够健壮!

技巧五:举例画图,辅助思考

找一个具体的例子,把它画下面释放一些脑容量,留更多的给逻辑思考,这样就会感觉到思路清晰很多。比如往单链表中插入一个数据这样一个操作,可以把各种情况都举一个例子,画出插入前和插入后的链表变化,如图所示:

c8e91f97954ef44f7978c9cc37819ec3.png

看图写代码,是不是就简单多啦?而且,当我们写完代码之后,也可以举几个例子,画在纸上,照着代码走一遍,很容易就能发现代码中的 Bug。

技巧六:多写多练,没有捷径

下面5个常见的链表操作。只要把这几个操作都能练熟,虽然说我已经写了不下2遍,但想要完整写出来来还是存在很多Bug的。将在下讲详解这五个例子

  • 单链表反转
  • 链表中环的检测
  • 两个有序的链表合并
  • 删除链表倒数第 n 个结点
  • 求链表的中间结点

写链表代码是最考验逻辑思维能力的。因为,链表代码到处都是指针的操作、边界条件的处理,稍有不慎就容易产生 Bug。链表代码写得好坏,可以看出一个人写代码是否够细心,考虑问题是否全面,思维是否缜密。

解决上讲遗留下的思考题:

回文字符串(提示:输入"aba",输出:True )

假设一:如果字符串时以数组来存储,如何判断是一个回文串?

思路:

  • 使用两个指针分别为left和right分别在最头端和最尾端
  • 当left < right 时进入到循环体,还得判断字母的大小写,考虑是否有非法字符

代码如下:

class Solution:
    def isPalindrome(self, s: str) -> bool:
        # 解决此类方法首选指针法,对于标点符号和空格可以省略 使用.isslnum()
        # 如果出现大写字母需要把大写字母转化成小写字母
         left, right = 0, len(s) - 1
         while left < right:
              while left < right and not s[left].isalnum():   
                    left += 1
              while left < right and not s[right].isalnum():
                     right -= 1

              # 判断大小写
              if s[left].lower() != s[right].lower():
                   return False
              
               left += 1
               right -= 1
          return Turn

假设二:如果字符串是通过单链表来存储的,如何判断是一个回文串?

思路:

  • 使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。这样当快指针指向末尾时,慢指针指向了中点。
  • 在慢指针前进的过程中,同时修改其 next 指针指向上一个元素prev,使得链表前半部分反序。
  • 最后比较中点两侧的链表是否相等。

代码如下:

class 

解决开篇问题:

解决今天最后一个问题:用python实现一个单向链表的基本操作,我个人觉得我写的代码不是那么好。

#coding=utf-8
##节点实现
# class StrinigNode(object):
#     #单链表的结点
#     def __init__(self,item):
#         #_item存放数据元素
#         self.item = item
#         #_next是下一个节点的标识
#         self.next = None
# from boto.roboto import param


class Node(object):
    #节点
    def __init__(self,elem):
        #elem(元素)保存到节点区
        self.elem = elem#用作保存数据
        #假设下一个标识为空
        self.next = None

# node =Node(100)

class SingleLinkList(object):
    #头节点为对象属性,定义在对象中

    def __init__(self,node=None):
        #1.假设链表中为空链表,则为None
        #2.假设在构造中用户传入了头节点,在Node中传入的头节点为100
        self.__head = node#头节点为自己内部函数使用,所以为私有的属性

    def is_empty(self):
        #判断链表是否为空
        #分析:当头节点为空时则表示链表为空
        return self.__head == None

    def length(self):
        # 链表的长度
        #cur游标,用来移动遍历节点
        cur = self.__head #从头节点开始遍历
        #count用来记录遍历多少次
        count =0
        while cur!=None:#在这的条件有两种选择:cur.next!=None/cur!=None
            '''为什么不选择cur.next!=None,当在最后一个数值时候指针域为None,在数据域中有数值将会漏掉这一个值
            所以还是采用cur!=None
            '''
            count+=1
            cur = cur.next
        return count#此方法已经解决了当头节点为空的情况

        # count = 1
        # while cur.next!= None:
        #     count+=1
        #     cur = cur.next
        # return count要考虑当指针为零的时候的情况


    def travel(self):
        # 遍历整个列表
        #遍历是与链表的长度判断是相似的,不过要考虑链表是否为空链表
        cur = self.__head
        while cur!=None:
            #那么就打印出链表的值
            print(cur.elem,end=',')
            #判断游标的下一个指针域
            cur = cur.next

    def add(self,item):
        #列表头部添加元素
        node=Node(item)#创建对象
        #保证链表的完整性,所以要先从新的元素开始操作
        node.next=self.__head
        self.__head=node

        #1.要考虑链表为空链表的时候,该算法是符合链表为空链表的时候


    def append(self,item):
        '''
        在链表尾部添加一个元素 ,尾插法
        node = Node(item)#在头节点传入一个元素
        ##判断链表是否为空链表
        #直接使用empty()方法
        if self.is_empty():
            self.__head=node
        else:
            cur = self.__head = None
            while cur.next!= None:#当出现头节点为空的时候是没有None值的
                cur = cur.next
                cur.next=node
                '''
        """链表尾部添加元素, 尾插法"""
        node = Node(item)
        if self.is_empty():
            self.__head = node
        else:
            cur = self.__head
            while cur.next != None:
                cur = cur.next
            cur.next = node

    def insert(self,pos,item):
        # 在指定位置pos添加节点
        #:param pos从0开始 ,par表示元素的前驱,也就是每个数最前面的一个节点
        ##需要判读头插法和尾插法两种情况
        if pos < 0:
            self.add(item)#item代表元素
        elif pos > (self.length()-1):
        #如果>= 是在length最后一个前面进行添加的
            self.append(item)
        else:
            pre = self.__head#
            count = 0 #用作计数
            while count < (pos-1):#不能包含==号
                count = +1
                pre = pre.next
            #当循环退出后 ,pre指向pos-1位置
                node=Node(item)
                node.next = pre.next
                pre.next = node



    def remove(self,item):
        # 删除一个例程,指的是删除数据,不是下标
        cur = self.__head
        pre = None

        while cur != None:
            if cur.elem == item:#判断你所需要的值是不是你所要的

                #再判断此节点是否是头节点
                #如果是
                if cur == self.__head:
                    self.__head = cur.next
                else:
                    pre.next = cur.next
                break
            else:
                cur = pre
                cur = cur.next
        #思考:1.链表为空链表,那么符不符合    2.删除的节点为首节点
        # 3.头部只有一个节点  4.尾部节点处理情况
    def search(self,item):
        #查找节点是否存在
        #判断数据是否在链表中,如果在返回True 不在返回False,进行遍历
        pre = self.__head #在这记住要判断该链表是否为空
        #先进行遍历
        while pre != None:
            #由于一开始在头节点上有elem元素,所以可以直接进行匹配
            if pre.elem == item: #进行判断
                return True
            #如果没有找到那就继续往下移动进行查找
            else:
                pre = pre.next
        return False
if __name__ == "__main__":
    ll = SingleLinkList()
    print(ll.is_empty())
    print(ll.length())
    ll.append(1)
    print(ll.is_empty())
    print(ll.length())
    ll.append(2)
    ll.add(8)
    ll.append(3)
    ll.append(4)
    ll.append(5)
    ll.append(6)
    # 8 1 2 3 4 5 6
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java可以使用以下代码来实现以带头结点循环链表表示队列: ```java public class Queue<T> { private Node<T> head; private int size; public Queue() { head = new Node<>(null); head.next = head; size = 0; } public void enqueue(T item) { Node<T> newNode = new Node<>(item); newNode.next = head.next; head.next = newNode; size++; } public T dequeue() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty"); } Node<T> nodeToRemove = head.next; head.next = nodeToRemove.next; size--; return nodeToRemove.data; } public T peek() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty"); } return head.next.data; } public boolean isEmpty() { return size == 0; } public int size() { return size; } private static class Node<T> { T data; Node<T> next; Node(T data) { this.data = data; this.next = null; } } } ``` 在这个实现中,`Queue`类有一个`head`属性,它是一个带有`null`数据的空节点,用来作为循环链表头结点队列的大小由`size`属性记录。 `enqueue`方法将一个节点插入到头结点之后,然后更新`size`。 `dequeue`方法从队列的头部删除一个节点,然后更新`size`并返回被删除节点的数据。如果队列为空,则抛出`NoSuchElementException`异常。 `peek`方法返回队列头部的节点的数据,但不删除该节点。如果队列为空,则抛出`NoSuchElementException`异常。 `isEmpty`方法检查队列是否为空。 `size`方法返回队列的大小。 `Node`是一个私有静态嵌套类,表示队列中的节点。每个节点都有一个`data`属性和一个指向下一个节点的`next`属性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值