美文网首页
数据结构之链表篇

数据结构之链表篇

作者: OPice | 来源:发表于2020-04-17 16:14 被阅读0次

    概念

    1.和数组一样,链表也是一种线性表。

    2.从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。

    3.链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。

    注意:掌握好技巧,会提高正确率,真正理解后,举一反三。将常见算法深刻理解之后,大多数链表算法将不再是你的障碍。

    技巧

    • 引用的理解
    • 引用丢失和内存泄漏
    • 边界判断
    • 利用哨兵(哑节点)

    引用的理解

    例如: a和b节点中插入x节点
    x.next = a.next;
    a.next = x;
    //其中x.next引用存储了a的下个节点next 的内存地址
    // a.next 引用储存的是x节点的内存地址
    

    引用丢失和内存泄漏

    //上面是先将节点b 的内存地址同时给多个引用,然后在改变a.next的引用
    //如果我们先改变a.next的应用
    a.next = x;
    x.next = a.next
    //这样会产生的现象是,a.next 指向了x的地址,但是b的地址就无法找到了。
    //然后x.next = a.next; 这时a.next 已经是x的地址了。相等于x.next = x,造成内存泄漏。
    
    image.png

    边界判断

    //1、链表为空
    /**
    * 尾插入元素是,如果头节点为null,上面的代码就不适用了,需要对节点做一个判断
    **/
    if(a == null){
        a = x; 
    }else{
        Node temp = a;
        while(temp.next != null){
            temp = temp.next;
        }
        temp.next = x;
    }
    return a;
    //删除一个节点 a.next = a.next.next;但是如果删除的是B节点,显然也需要做判断
    //2、链表只有一个元素、两个元素
    //3、头节点和尾节点的处理
    

    利用哨兵

    // 上面在写链表的处理的时候,插入新节点,有两种代码逻辑,一种是头节点null、一种是不为null,逻辑是不一样的
    // 那么我们可以考虑另一种方式,创建一个哨兵节点,这个节点什么元素都不存。只是简化边界判断
     Node temp;
     Node head = new Node();
     head.next = a;
     temp = head;
     while (temp.next != null) {
        temp = temp.next;
     }
     temp.next = x;
     return head.next;
    //这样的代码就可以处理,那两种情况,同理删除元素
    

    常见算法

    • 反转链表
    • 合并有序链表
    • 删除倒数第n个节点
    • 链表中间节点
    • 环状链表检测

    反转

    // 输入:1->2->3->4    输出:4->3->2->1
    // 思路: 1、迭代所有节点,取当前节点    2、取当前节点的前一个节点  3、当前节点的next 指向前一个节点 
    // 实现:
    Node reserver(Node curr) {
      Node pre = null; //作为全局变量,保证下次遍历时能访问到,初始值null,因为第一次遍历头节点的下个节点是null
      while(curr != null){
        //头节点(1)的前一个节点是null:curr.next = null; 
        //但是下一个节点(2)的前一个节点是头节点(1) curr.next =?,所以我们需要将前一个节点保存起来,下次迭代还能访问
        curr.next = pre; // 当前节点curr的下个节点指向上一个 pre
        pre = curr; //将当前节点给pre预存起来,保证当前处理不会改变,下次迭代时使用,
                    //如果没有下次迭代,那么pre就是反转后的头节点
        curr = curr.next; 
      }
      return pre;
    }
    // 最后: 如果按照上面的代码去执行,会发现根本不是想要的结果,1->null
    // 原因: curr.next = pre 这行,curr的下个节点已经被改变了(null),所以 curr = curr.next 其实等于curr = pre;
    // 修改: 将curr原始下个节点预存,给一个变量 temp,然后curr = temp;
    Node reserver(Node node) {
        Node pre = null; //作为全局变量,保证下次遍历时能访问到,初始值null,因为第一次遍历头节点的下个节点是null
        while(node != null){
            //头节点(1)的前一个节点是null:curr.next = null;
            //但是下一个节点(2)的前一个节点是头节点(1) curr.next =?,所以我们需要将前一个节点保存起来,下次迭代还能访问
            Node temp = node.next;
            node.next = pre; // 当前节点curr的下个节点指向上一个 pre
            pre = node; //将当前节点给pre预存起来,保证当前处理不会改变,下次迭代时使用
            node = temp;
        }
        return pre;
    }
    

    排序

    合并排序

    //两个有序链表合并一个有序链表
    // 其中有一个为null,或者都为null
    if(l1 == null) {
        return l2;
    }
    if(l2 == null) {
        return l1;
    }
    ListNode listNode = new ListNode(-1);
    ListNode pre = listNode;
    while(l1 != null && l2 != null){
        if(l1.val <= l2.val){
            pre.next = l1;
            l1 = l1.next;
        }else{
            pre.next = l2;
            l2 = l2.next;
        }
        pre = pre.next;
        return listNode.next;
    }
    

    删除倒数第n个节点

    //统计链表长度
    ListNode removeNthFromEnd(ListNode head, int n) {
        int length  = 0;
        ListNode first = head;
        while (first != null) {
            length++;
            first = first.next;
        }
        //移除元素的前个节点的位置(length-=n)
        //例如:1->2->3->4->5,5个元素移除倒数第2个节点(4),也就是3=5-2是4的前节点。只要将3 ->5 就完成了删除。
        length -= n;
        //定义一个带有头节点的链表
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        first = dummy;
        while (length > 0) {
            length--;
            first = first.next;
        }
        //如果length = 0,first = 3节点。所以将3.next = 3.next.next;
        first.next = first.next.next;
        return dummy.next;
    }
    //最后:边界判断 需要考虑,上面默认是参数是合法有效
    //1、链表为null,head=null,n=0 其中first.next =first.next.next 相当于 head = head.next,head=null,所以会npt
    //2、链表为一个元素/两个元素 
    //3、头/尾节点
    //4、n值非法的情况
    

    链表的中间节点

    // 听到中间立马想到,快慢指针
    public ListNode middleNode(ListNode head) {
      ListNode p = head; ListNode q = head;
      while(q!=null && q.next!=null){
          p = p.next;
          q = p.next.next;
      }
      return p;
    }
    

    链表中环检测

    public static boolean hasCycle(ListNode head) {
         // 如果2个节点以上,有循环链表,现象:迭代一直会走下去。如何判断有循环,第一个想法某个节点迭代了两次。
         // 1->2->3->4 4又指向了1 环就形成了,如果依次迭代,将之前元素放入集合,如果集合中存在,则又环
         Set<ListNode> listNodes = new HashSet<>();
         while (head!=null){
             if (listNodes.contains(head)){
                 return true;
             }
             listNodes.add(head);
             head = head.next;
         }
         return false;
    }
    //第二种 方式,就是使用快慢指针,想一下,如果存在环的话。走的快的人肯定会跟走的慢的相遇,
    //想一下操场上,有一个人速度是你的两倍,如果你跑完一圈,那个人肯定会跟你相遇。
    //如果你跑到终点了,还没和你没有相遇说明人家早到终点,停下来了。
    public static boolean hasCycle(ListNode head) {
         // 判断边界 ,1、head = null 2、head.next = null 3、head.next.next =null 4、头尾节点。
         // 如果null 或者只有一个节点是不能形成循环节点(本身指向本身节点除外)
        if(head == null || head != null){
            return false;
        }
        // 是否是统一起点没有关系,如果是环肯定会相遇
        ListNode p = head; ListNode q = head.next;
        while(q != null && q.next != null){
            if(p == q){
                return true;
            }
            p = p.next;
            q = q.next.next;
        }
        return false;
    }
    

    相关文章

      网友评论

          本文标题:数据结构之链表篇

          本文链接:https://www.haomeiwen.com/subject/bsfpuhtx.html