数据结构之链表篇

概念

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 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;
}

你可能感兴趣的:(数据结构之链表篇)