数据结构之链表

156 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 23 天,点击查看活动详情

日积月累,水滴石穿 😄

链表

链表是一种物理存储单元上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点(链表中每一个元素称为节点)组成,每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

来源于百度百科

链表类别

  • 单链表
  • 循环单链表
  • 双链表
  • 循环双链表

链表优点

  • 链表插入和删除速度快,只需要修改相邻节点的指针域的指向。
  • 可以充分利用计算机内存空间,不必预先知道数据大小。

链表缺点

  • 存储数据同时还需要额外存储下一个节点的地址(单链表,双链表还要维护上一个节点地址),空间开销比较大。
  • 不能像数组那样直接找到指定节点,需要从链表的头节点或尾节点进行遍历,查询速度较慢。

单链表

链表中的每个节点都有一个指针指向下一个节点,最后一个节点的 next 指向 NULL。这个就是单链表。

image.png

  • item:用来存放元素
  • next:用来指向下一个节点元素 从图中可以发现,有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们把第一个结点叫作头结点,把最后一个结点叫作尾结点。 其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。

伪代码结构

private  class Node<E> {
	E item;
	Node<E> next;
}

循环单链表

循环单链表其实就是单链表,区别在于最后一个节点的 next 指向头节点,整个链表形成一个环。

image.png

双链表

双链表是对单链表的改进,双链表与单链表相比,多了一个存储上一个结点地址的指针域,名为 prev。双向链表需要额外的一个空间来存储前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然多了一个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。

image.png 看图可以知道,双向链表是包含两个指针的,prev 指向前一个节点,next指向后一个节点,但是第一个节点也就是头节点的prev指向 null,最后一个节点也就是尾节点指向 null。

伪代码结构

private  class Node<E> {
	E item;
	Node<E> next;
        Node<E> prev;
}

循环双链表

image.png 第一个节点也就是头节点prev指向尾节点,最后一个节点也就是尾节点 next指向头结点,形成环。

自定义单链表

上面讲了这么多,开始我们的实战,直接上代码。

public class MyLinkedList {
    
    private MyNode node;
    int size = 0;
}

class MyNode{

    String item;		//值
    MyNode next;	//下一个节点的指针

    MyNode(String item){
        this.value = value;
        this.next = null;
    }
}
  • MyNode:这个就是节点。节点中有值、有下一个节点的指针。
  • size:记录链表节点的个数。

addHead

添加节点到链表头部。

/**
 * 插入链表头部
 */
public void addHead(String data){
	MyNode newNode = new MyNode(data);
	// head = myNode;  如果head 为 null,可以这么写
	// 但如果 head有值了呢?再插入一个值到头部,原有的值就会被覆盖
	newNode.next = head;
	head = newNode;
	size++;
}

image.png

add

/**
 * 添加节点到链表尾部
 */
public void add(String data){
	MyNode newNode = new MyNode(data);
	MyNode cur = head;
	//遍历到末尾元素 末尾元素 next 指向 null
	while (cur.next != null){
		cur = cur.next;
	}
	//直接将末尾的 next 指向新元素
	cur.next = newNode;
	size++;
}

获得末尾节点,将末尾节点的 next 指向新节点。

add(int index,String data)

指定下标添加节点

/**
 * 指定下标添加链表节点
 */
public void add(int index, String data) {
  //下标越界
  if(index > size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
  }
  //判断是否添加的是头结点
  if(index == 0) {
    addHead(data);
  }
  else {
    // 如果不是头节点,则需要进行遍历,获得下标前一个节点
    //比如源链表 1、2、3、4 ,在下标为 2 插入节点 5
    //目标链表为 1、2、5、3、4
    //1、因为单链表只有 next 指针,所以要找到指定下标节点的前一个节点
    //2、如果 index 不减 1,当需要在尾部添加节点时,会出现空指针
    MyNode cur = head;
    for(int i = 0; i < index - 1; i++) {
      cur = cur.next;
    }
    MyNode newNode = new MyNode(data);
    //将下标节点与新节点关联起来
    newNode.next = cur.next;
    //next 指向新节点
    cur.next = newNode;
    size++;
  }
}

指定下标添加节点方法,想对于添加末尾方法,可以说就多了一行新代码:newNode.next = cur.next;,需要维护当前插入节点的下个节点是什么,而插入到尾部则不需维护。

remove

/**
 * 根据下标删除节点
 */
public void remove(int index) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  MyNode cur = head;
  //如果删除头节点
  if(index == 0) {
    removeHead();
    return;
  }
  //找到指定下标节点的前一个节点
  for(int i = 0; i < index - 1; i++) {
    cur = cur.next;
  }
  //跳过要删除元素的指向
  cur.next = cur.next.next;
  size--;
}

removeHead

/**
 * 删除链表头节点
 */
private void removeHead() {
  if(head == null) {
    return;
  }
  //获得头节点的下个节点
  MyNode headNext = head.next;
  //将下个节点赋值给头节点
  head = headNext;
  size--;
}

removeTail

 /**
  * 删除链表尾节点
  */
 public void removeTail() {
   remove(size - 1);
 }

set

/**
 * 修改指定下标节点值
 */
public void set(int index, String data) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  //获得指定下标节点
  MyNode node = getNode(index);
  node.item = data;
}

get

/**
 * 获得指定下标节点的值
 */
public String get(int index) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
  }
  //获得指定下标节点
  MyNode node = getNode(index);
  return node.item;
}

print

public void print() {
    MyNode cur = head;
    while(cur != null) {
      System.out.println(cur.item + " ");
      cur = cur.next;
    }
  }

getNode

获得指定下标节点

  /**
   *  获得指定下标节点
   */
private MyNode getNode(int index) {
  MyNode cur = head;
  for(int i = 0; i < index; i++) {
    cur = cur.next;
  }
  return cur;
}

测试数据

public static void main(String[] args) {
  MyLinkedList myLinkedList = new MyLinkedList();
  System.out.println("添加头节点");
  myLinkedList.addHead("2");
  myLinkedList.addHead("1");
  myLinkedList.print();
  System.out.println("在尾部添加节点");
  myLinkedList.add("3");
  myLinkedList.add("4");
  myLinkedList.print();
  System.out.println("指定下标添加节点");
  myLinkedList.add(0, "666");
  myLinkedList.add(5, "777");
  myLinkedList.add(2, "888");
  myLinkedList.print();
  System.out.println("指定下标删除节点");
  myLinkedList.remove(0);
  myLinkedList.remove(5);
  myLinkedList.removeHead();
  myLinkedList.removeTail();
  myLinkedList.print();

  System.out.println("指定下标修改节点");
  myLinkedList.set(0, "cxyxj");
  myLinkedList.print();
  System.out.println("指定下标获得节点");
  System.out.println(myLinkedList.get(0));

}

各位小伙伴,图我就没划了,太麻烦了,也太耗时间了,可能我也画不出来。各位自己下去用 1 -> 2 -> 3 -> 4 -> 5替代吧。

自定义双链表

/**
 * @author cxyxj
 */
public class MyDoubleLinkedList {
  /**
   * 头节点
   */
  MyDoubleNode headNode;
  /**
   * 尾节点
   */
  MyDoubleNode tailNode;
  /**
   * 节点的个数
   */
  int size;

  private static class MyDoubleNode {
    /**
     * 值
     */
    String item;
    /**
     * 下一个节点的指针
     */
    MyDoubleNode next;
    /**
     * 上一个节点的指针
     */
    MyDoubleNode prev;

    MyDoubleNode(MyDoubleNode prev, String data, MyDoubleNode next) {
      this.item = data;
      this.next = next;
      this.prev = prev;
    }
  }

  public void print() {
    MyDoubleNode cur = headNode;
    while(cur != null) {
      System.out.println(cur.item + " ");
      cur = cur.next;
    }
  }

}

addHead

添加节点到头部

/**
 * 添加节点到头部
 */
public void addHead(String data) {
  MyDoubleNode head = headNode;
  //在头节点插入元素,上一个元素的指针肯定是 null 的
  MyDoubleNode newNode = new MyDoubleNode(null, data, head);
  //head 为 null,代表链表中没有元素
  if(head == null) {
    //尾节点也是新节点
    tailNode = newNode;
  } else {
    //prev 指向新节点
    head.prev = newNode;
  }
  //将新节点做为头节点
  headNode = newNode;
  size++;
}

add

添加节点到链表尾部

/**
 * 添加节点到链表尾部
 */
public void add(String data) {
  MyDoubleNode tail = tailNode;
  MyDoubleNode newNode = new MyDoubleNode(tail, data, null);
  tailNode = newNode;
  if(tail == null) {
    headNode = newNode;
  } else {
    tail.next = newNode;
  }
  size++;
}

add(int index, String data)

指定下标添加链表节点

/**
 * 指定下标添加链表节点
 */
public void add(int index, String data) {
  //下标越界
  if(index > size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  //如果添加的是尾部元素
  if(index == size) {
    add(data);
  } else {
    //获得指定下标节点
    MyDoubleNode node = getNode(index);
    //获得下标节点的上个节点
    MyDoubleNode prev = node.prev;
    //构建新节点 新节点已经关联了上一个节点和下一个节点关系 只需要再关联 上一个节点和下一个节点 与新节点的关系
    MyDoubleNode newNode = new MyDoubleNode(prev, data, node);
    //将下标节点的上个指针指向新节点
    node.prev = newNode;
    //当 index 为 0 时,prev 是为 null 的,也就是添加头节点
    //将新节点给 headNode
    if(prev == null) {
      headNode = newNode;
    } else {
      //将下标节点的上个节点的 next 指针指向新节点
      prev.next = newNode;
    }
    size++;
  }
}

//这个获得指定下标的元素的代码是可以进行优化的
//各位小伙伴可以去看看 LinkedList 的实现
private MyDoubleNode getNode(int index) {
  //由于有了上一个元素的指针,可以从后向前遍历
  MyDoubleNode cur = headNode;
  for(int i = 0; i < index; i++) {
    cur = cur.next;
  }
  return cur;
}

remove

/**
 * 删除指定下标节点
 */
public void remove(int index) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  MyDoubleNode node = getNode(index);
  MyDoubleNode prev = node.prev;
  MyDoubleNode next = node.next;
  // prev 为 null,代表 index = 0
  // 将要删除节点的下一个节点作为 headNode
  if(prev == null) {
    headNode = next;
  } else {
    //跳过要删除节点的指向, 将要删除节点的上一个节点 指向 要删除节点的下一个节点
    prev.next = next;
    //方便GC
    node.prev = null;
  }
  // next 为 null,代表 index 为最大的下标,也就是要删除尾部节点
  // 将要删除节点的上一个节点作为 tailNode
  if(next == null) {
    tailNode = prev;
  } else {
    //跳过要删除节点的指向, 将要删除节点的下一个节点 指向 要删除节点的上一个节点
    next.prev = prev;
    //方便GC
    node.next = null;
  }
  node.item = null;
  size--;
}

removeHead

删除链表头节点

/**
 * 删除链表头节点
 */
private void removeHead() {
  //remove(0);
  if(headNode == null) {
    return;
  }
  //获得头节点的下个节点
  MyDoubleNode headNext = headNode.next;
  //方便GC
  headNode.item = null;
  headNode.next = null;

  //将下个节点赋值给头节点
  headNode = headNext;
  //如果头节点的下个节点为 null,说明链表中就一个节点
  //删除了这个节点,尾节点也要为 null
  if(headNext == null) {
    tailNode = null;
  } else {
    //链表中有多个节点,headNext 现在为 头节点,头节点的 prev 指针需要指向 null
    headNext.prev = null;
  }
  size--;
}

removeTail

删除链表尾节点

/**
 * 删除链表尾节点
 */
public void removeTail() {
  // remove(size -1);
  if(tailNode == null) {
    return;
  }
  //获得尾节点的上个节点
  MyDoubleNode tailPrev = tailNode.prev;
  //方便GC
  tailNode.item = null;
  tailNode.prev = null;
  //将上个节点赋值给尾节点
  tailNode = tailPrev;
  //如果尾节点的上个节点为 null,说明链表中就一个节点
  //删除了这个节点,头节点也要为 null
  if(tailPrev == null) {
    headNode = null;
  } else {
    //链表中有多个节点,tailPrev 现在为尾节点,尾节点的 next 指针需要指向 null
    tailPrev.next = null;
  }
  size--;
}

set

修改指定下标节点值

/**
 * 修改指定下标节点值
 */
public void set(int index, String data) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  //获得指定下标节点
  MyDoubleNode node = getNode(index);
  node.item = data;
}

get

获得指定下标节点的值

/**
 * 获得指定下标节点的值
 */
public String get(int index) {
  //下标越界
  if(index >= size || index < 0) {
    throw new IndexOutOfBoundsException("Index: " + index +
      ", Size: " + size);
  }
  //获得指定下标节点
  MyDoubleNode node = getNode(index);
  return node.item;
}

测试数据

public static void main(String[] args) {

    MyDoubleLinkedList linkedList = new MyDoubleLinkedList();
    
    System.out.println("添加到头部");
    linkedList.addHead("22");
    linkedList.addHead("11");
    linkedList.print();
    System.out.println("添加到尾部");
    linkedList.add("33");
    linkedList.print();
    System.out.println("指定下标添加");
    linkedList.add(0,"555");
    linkedList.add(2,"55544");
    linkedList.add(5,"666");
    linkedList.print();
    System.out.println("指定下标删除");

    linkedList.remove(1);
    linkedList.remove(1);

    linkedList.print();
    System.out.println("删除头尾节点");
    linkedList.removeHead();
    linkedList.removeTail();
    linkedList.print();
    System.out.println("指定下标修改节点");
    linkedList.set(0,"cxyxj");
    linkedList.print();
    System.out.println("获得指定下标节点");
    System.out.println(linkedList.get(0));
}

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。