前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据结构之链表篇

数据结构之链表篇

作者头像
OPice
发布2020-04-23 15:32:47
3770
发布2020-04-23 15:32:47
举报

概念

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;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概念
  • 技巧
    • 引用的理解
      • 引用丢失和内存泄漏
        • 边界判断
          • 利用哨兵
          • 常见算法
            • 反转
              • 排序
                • 删除倒数第n个节点
                  • 链表的中间节点
                    • 链表中环检测
                    相关产品与服务
                    数据保险箱
                    数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档