【Java】轻松掌握链表操作

1.1 什么是链表

在数组存储的过程中,数组的存储必须使用连续的内存空间,并且会预留一部分空间方便扩展。这样在内存中会大大降低内存的使用率,这种事情是不被接受的。所以出现了一种新的数据结构,叫作——链表

综上可知:链表的发明其实是对数组的一种补充。因此它的功能和数组非常类似,都是用于存储一系列相同类型的数组,并且都有增删改查等基本操作

用一句话来总结:

数组能做的所有事情,一般都可以用链表实现

只是他们的存储方式不同,导致他们的实现逻辑也不同,同样时间复杂度也将不同。下面我们来分析一下链表的特性:

节点

数组里面每个元素我们称之为元素,而链表中每个元素我们称为节点

在这里插入图片描述

从图中看出,每个节点都是由两部分组成 — 节点内容和下一个节点地址。元素和节点的差别就在这个下一个节点地址

在数组中,由于是连续内存空间存储,我们可以通过 start_address + item_size * i 计算元素的位置。而在链表中,存储空间是分散存储的,所以我们需要每个节点保存下一个节点的位置。也就是可以通过节点 1 找到节点 2,节点 2 找到节点 3…依次遍历所有的节点信息

从这个结构,可以看出链表对于数组最大的好处,它可以将数据分散存储在内存中,达到内存最大利用率

代码创建链表:

public class Node {

	private int content;

  	private Node next;

  	public Node() {
  	}

  	public Node(int content, Node next) {
    	this.content = content;
    	this.next = next;
  	}

  	public int getContent() {
    	return content;
  	}

  	public void setContent(int content) {
    	this.content = content;
  	}

  	public Node getNext() {
    	return next;
  	}

  	public void setNext(Node next) {
    	this.next = next;
  	}
}
// 创建链表
public static Node createLinkedList(int[] array) {
    // 创建一个根节点
    Node root = null;
    // 从末尾元素开始依次创建Node节点
    for (int i = array.length - 1; i >= 0; i--) {
        Node node = new Node(array[i], null);
        // 创建两个节点的连接关系
        if (root != null) {
            node.setNext(root);
            root = node;
        } else {
            root = node;
        }
    }
    // 返回根节点
    return root;
}

1.2 链表的读取与查找

链表中索引内容读取

看一下数组是如何进行数据的读取的:

因为数组是连续内存空间存储的,通过 start_address + item_size * i 便能计算出索引地址,然后 1 步便完成内容的读取,时间复杂度是 O(1)

那链表中应该如何操作呢?比如我们要找到 9, 2, 4 链表中索引为 2 的存储内容?

计算机肯定是无法一步完成这个操作,因为链表在内存中是分散存储的,我们现在只有第一个节点的地址,因此需要依次遍历找到第 3 个节点的地址

在这里插入图片描述

时间复杂度

从上图可以看出,链表中的读取时间复杂度和索引值有关。最好的情况是读取第一个节点,只需要一步。因此链表的读取时间复杂度为 O(N),相比较数组性能可能会差一点

链表中遍历查找元素

如果想要在链表 9, 2, 4 中查找是否包含某个节点它的内容是4,该如何操作?

我们同样需要利用第一个节点,依次往后面遍历,每次遍历到一个节点,比较这个节点内容是否为 4

在这里插入图片描述

逻辑很简单,所需步数和读取索引值一样,最好的情况在第一个节点就可以查找到,著需要1步。最差的情况是在最后一个节点才查找到,需要N步

所以时间复杂度为 O(N),查找元素的时间复杂度和数组是一样的

代码如下:

public class OrangeLinkedList {

  	private Node root = null;
  	int size = 0;

  	public OrangeLinkedList() {
  	}

  	// 数组创建一个链表
  	public static Node createLinkedLNode(int[] array) {
    	// 创建一个根节点
    	Node root = null;
    	// 从末尾元素开始依次创建Node节点
    	for (int i = array.length - 1; i >= 0; i--) {
      		Node node = new Node(array[i], null);
      		// 创建两个节点的连接关系
      		if (root != null) {
        		node.setNext(root);
        		root = node;
      		} else {
        		root = node;
      		}
    	}
    	return root;
  	}

  	public orangeLinkedList(int[] array) {
    	this.root = YKDLinkedList.createLinkedLNode(array);
    	this.size = array.length;
  	}

  	// 获取长度
  	public int size() {
    	return this.size;
  	}

  	// 获取某个索引节点内容
  	public int get(int index) {
    	int i = 0;
    	Node node = this.root;
    	// 依次往前推进
    	while (i < index) {
      		node = node.getNext();
      		i++;
    	}
    	return node.getContent();
  	}

  	// 查找某个值的索引值,默认不存在为-1
  	public int find(int value) {
    	Node node = this.root;
    	// index 保存当前的索引
    	int index = 0;
    	// 依次遍历链表,找到内容等于value的node,返回index
    	while (node != null) {
      		if (node.getContent() == value) {
        		return index;
      		}
      		node = node.getNext();
      		index++;
    	}
    	return -1;
  	}

    // 提供数据进行测试
  	public static void main(String[] args) {
    	int[] array = {1, 2, 3};
    	orangeLinkedList linkedList = new OrangeLinkedList(array);
    	System.out.println(linkedList.find(2));
    	System.out.println(linkedList.find(8));
    	System.out.println(linkedList.get(2));
  	}
}

1.3 链表的插入


头部插入分两部分执行:

  1. 新节点的 next 指向原来的 root 节点
  2. root 指针指向新节点

时间复杂度很容易计算出来为 O(1)


同理,中间插入也是分为两部分执行:

  1. 新节点 next 指向原来前置节点的 next
  2. 原来前置节点的 next,指向新节点

看起来也只需要 O(1) 就能完成插入,但是这样的插入有一个前置条件,也就是我们知道插入节点的前置节点。否则,我们只能根据索引通过 N步 找到前置节点


尾部插入和中间插入的逻辑完全一样。只是中间插入的时候前置节点的 next 是一个节点,而此处为 null:

  1. 新节点 next 指向原来前置节点的 next(此处为null)
  2. 原来前置节点的 next,指向新节点

通过上面的对比,我们可以总结如下:

如果已经知道插入节点的前置节点,那么链表的插入时间复杂度为 O(1) 普通情况(根据索引值插入节点),插入的时间复杂度为 O(N) 这个和数组是一样的。但有趣的是,链表插入的最好和最差的情况刚好和数组相反,链表在头部插入很方便,但是数组开头插入确很麻烦。而数组在尾部插入很方便,但链表尾部插入要先扫描再插入


1.4 链表的删除

头部删除分为两步执行:

  1. root 指针指向第二个节点
  2. 原始 root 节点的 next 设置为 Null

时间复杂度很容易计算出来为 O(1)

同理,中间删除也分为两部分执行

  1. 待删除节点前置节点 next 指向待删除节点后置节点
  2. 待删除节点的 next,指向 Null

看起来好像也只需要 O(1) 就能完成删除,但是这样的删除有一个前置条件,也就是我们知道插入节点的前置节点。否则,我们只能根据索引通过 N步 找到前置节点

尾部删除和中间删除的逻辑完全一样。只是中间删除的时候前置节点的 next 改为待删除节点的后置节点,而此处后置节点为 null

待删除节点原本的 next 为 null,所以不需要再赋值为空。但是,我们仍然必须需要执行 N步 找到倒数第二个节点,再执行上面的删除操作


总结:其实删除操作,只是插入操作的反操作,原理还是比较容易理解,我们总结一下链表删除的时间复杂度

如果已经知道待删除节点的前置节点,那么链表的删除时间复杂度为 O(1) 普通情况(根据索引值删除节点),因为需要新找到前置节点,所以时间复杂度为 O(N),这个和数组一样。和链表插入一样,链表删除的最好和最差的情况刚好和数组想反,链表在头部删除很方便,但是数组开头删除却很麻烦。而数组在尾部删除很方便,但链表尾部删除要先扫描再删除

链表插入和删除的代码如下:

public class OrangeLinkedList {

 	private Node root = null;
  	int size = 0;

  	public OrangeLinkedList() {
  	}

  	// 数组创建一个链表
  	public static Node createLinkedLNode(int[] array) {
    	// 创建一个根节点
    	Node root = null;
    	// 从末尾元素开始依次创建Node节点
    	for (int i = array.length - 1; i >= 0; i--) {
      		Node node = new Node(array[i], null);
      		// 创建两个节点的连接关系
      		if (root != null) {
        		node.setNext(root);
        		root = node;
      		} else {
        		root = node;
      		}
    	}
    	return root;
  	}

  	public OrangeLinkedList(int[] array) {
    	this.root = YKDLinkedList.createLinkedLNode(array);
    	this.size = array.length;
  	}

  	// 获取长度
  	public int size() {
    	return this.size;
  	}

  	// 获取某个索引节点内容
  	public int get(int index) {
    	int i = 0;
    	Node node = this.root;
    	// 依次往前推进
    	while (i < index) {
      		node = node.getNext();
      		i++;
    	}
    	return node.getContent();
  	}

  	// 查找某个值的索引值,默认不存在为-1
  	public int find(int value) {
    	Node node = this.root;
    	// index 保存当前的索引
    	int index = 0;
    	// 依次遍历链表,找到内容等于value的node,返回index
    	while (node != null) {
      		if (node.getContent() == value) {
        		return index;
      		}
      		node = node.getNext();
      		index++;
    	}
    	return -1;
  	}

  	// 末尾添加元素
  	public boolean add(int value) {
    	return this.add(this.size - 1, value);
  	}

  	// 头部插入元素
  	public boolean addFirst(int value) {
    	return this.add(-1, value);
  	}

  	// 插入元素
  	public boolean add(int index, int value) {
    	if (index < -1 || index > this.size - 1){
      		return false;
    	}
    	if (index == -1) {
      		if (this.root != null) {
        		this.root = new Node(value, this.root);
      	} else {
        	this.root = new Node(value, null);
    	} else {
      		Node pre = this.root;
      		while (index > 0) {
        		if (pre.getNext() == null) {
          			return false;
        		}
        		pre = pre.getNext();
        		index--;
      		}

      		if (pre == null) {
        		return false;
      		}

      		Node newNode = new Node(value, pre.getNext());
      		pre.setNext(newNode);
    	}
    	this.size++;
    	return true;
  	}

  	// 删除最后一个元素
  	public boolean removeLast() {
    	return this.remove(this.size - 1);
  	}

  	// 删除第一个元素
  	public boolean removeFirst() {
    	return this.remove(0);
  	}

  	// 插入元素
  	// index = 0, 表示删除第 1 个元素
  	// index = n,表示删除第 n+1 个元素
  	public boolean remove(int index) {
    	if (index < 0 || index > this.size - 1){
      		return false;
    	}
    	// 判断整个列表为空情况,删除失败
    	if (this.root == null) {
      		return false;
    	}
    	if (index == 0) {
      		// 删除第一个元素,情况比较简单,注意必须先保留 this.root,因为紧接着 this.root 会被修改
      		Node node = this.root;
      		this.root = node.getNext();
      		node.setNext(null);
    	} else {
      	// 判断越界问题
      	if(this.size < index + 1){
        	return false;
      	}
      	// 遍历获取index的前置节点
      	Node pre = this.root;
      	while (index > 1) {
        	pre = pre.getNext();
        	index--;
      	}
      		// 修改next指针,同样大家需要先保存 pre.next,因为紧接着下一步会修改 pre.next 指针
      		Node next = pre.getNext();
      		pre.setNext(next.getNext());
      		next.setNext(null);
    	}
    	this.size--;
    	return true;
  	}

  	// 链表中的元素字符串形式
  	public String toString() {
    	if (this.root == null) {
      		return "";
    	}

    	StringBuilder str = new StringBuilder();
    	Node node = this.root;
    	while (node != null) {
      		str.append(node.getContent()).append(" ");
      		node = node.getNext();
    	}
    	return str.toString();
  	}

  	public static void main(String[] args) {
    	int[] array = {10, 9, 2, 4};
    	OrangeLinkedList linkedList = new OrangeLinkedList(array);
    	boolean result = linkedList.removeFirst();
    	if (!result) {
      		System.out.println("removeFirst失败");
      		return;
    	}
    	result = linkedList.removeLast();
    	if (!result) {
      		System.out.println("removeLast失败");
      		return;
    	}
    	result = linkedList.remove(1);
    	if (!result) {
      		System.out.println("remove(1)失败");
      		return;
    	}

    	System.out.println(linkedList.toString());
  	}
}

1.5 链表 vs 数组

链表与数组在性能上有什么区别?

数组链表
存储连续内存存储分散a内存存储
读取O(1)O(N)
插入O(N)O(N)
插入最好情况数组末尾插入链表开头插入
插入最坏情况数组开头插入链表末尾插入
删除O(N)O(N)
删除最好情况数组末尾删除链表开头删除
删除最坏情况数组开头删除链表末尾删除

链表看似除了在存储方面有优势,好像其他都不如数组。但是在复杂场景下,表现并不是这样:

我们手里有一堆手机号码,我们希望写一个算法删除无效的手机号码。具体逻辑为:我们每次读取一个手机号码,判断这个手机号码是否为11为,并且满足一定的正则表达式要求

这种场景下应该用什么数据结构存储这一堆手机号码呢?

在这个场景下最关键的两个因素遍历删除

遍历

无论使用数组或者链表进行存储,都需要经历 N 次遍历。这部分无法减小

删除

如果使用数组删除的话,每次都需要移动后面的元素,所以每次删除的时间复杂度都是 O(N)

而如果使用链表的话,就不存在这个问题。因为在遍历场景下,我们都可以拿到需要删除节点的前置节点,所以在这个场景下的删除时间复杂度为 O(1)

总结

简单统计下,在这种场景下使用数组的操作的时间复杂度为 O(N^2)。而使用链表的时间复杂度为 O(N)

从这个例子看出链表在频繁插入和删除的情况下,有绝对的优势!


1.6 双向链表

双向链表的特点:

  1. 对于每个节点,由三部分组成,prev(指向前一个节点),next(指向后一个节点),content(节点内容)
  2. 对于双向链表,为了方便遍历,我们需要同时保留第一个节点和最后一个节点

在这里插入图片描述

阅读 LinkedList 源码,我们会发现它实际上就是利用 双向链表 实现的,在源码中我们可以找到如下代码:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

    ......
}

需要关注这里的两个变量 firstlast。分别表示双向链表的头部和尾部两个节点

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值