数据结构-线性表:链表


title: 数据结构-线性表:链表
tags: 数据结构,链表
renderNumberedHeading: true
grammar_cjkRuby: true

概述

线性表是由N个元素组成的有序排列,也是最常见的一种数据结构,主要包括:数组 和 链表

链表结构

链表是一种链式存储结构,存储的数据在磁盘上是分开的,只是因为它们之间有一些关联,多个节点之间通过地址进行连接,将其联系在一起就形成了链表。
链表主要分为以下几种:

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

链表结构分析

单向链表

结构:链表中的每个元素节点中除了存储自身元素外,还存储了下一个节点,用来指向下一个节点。

  • 简单实现一个单向链表:
public class Node {
    public String data;    // 存储自身元素
    public Node next;      // 存储下一个节点
    // 新建一个节点,默认下一个节点为null
    public Node(String data) {
        this.data = data;
        next = null;
    }
}
public class SingleChainList {
    private int size;   // 链表长度
    public Node head;   // 链表头节点
    public SingleChainList() {    // 初始化一个空链表
        size = 0;
    }
    public int size() {    // 获取链表长度
        return size;
    }

    /**
     * 打印单链表的所有值
     */
    public void print() {
        if (size == 0) {
            System.out.println("空列表");
            return;
        }
        Node node = head;
        while (node != null) {
            System.out.println(node.data);
            node = node.next;
        }
    }

    /**
     * 在末尾追加一个节点
     */
    public void add(Node node) {
        if (node == null) {
            return;
        }
        //如果链表为空,那新增的这个node就作为head
        if (size == 0) {
            head = node;    // 头节点指向当前节点
        } else { //添加到末尾
            Node last = head;    // 从头节点开始遍历
            while (last.next != null) {    // 遍历到最后一个节点
                last = last.next;
            }
            last.next = node;// 加当前节点添加到最后一个节点,即:最后一个节点执向它即可
        }
        size++;
    }
    /**
     * 在某个位置插入一个节点
     */
    public void add(int index, Node node) {
        if (index < 0 || index > size) {
            throw new RuntimeException("越界");
        }
        if (node == null) {
            return;
        }
        //插头部
        if (index == 0) {    // index=0表示插入在头部,即:将当前节点指向头节点,然后将当前节点作为头节点
            node.next = head;
            head = node;
        } else {
            Node tempNode = head;    // 从头节点开始遍历,找到插入的节点位置,将前一个节点指向当前节点,当前节点指向原前一个节点的下一个节点
            for (int i = 0; i < index - 1; i++) {
                //取到要被插入的节点的父节点
                tempNode = tempNode.next;
            }
            node.next = tempNode.next;
            tempNode.next = node;
        }
        size++;
    }
}
  • 单向链表图解
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ocTO2iYo-1617371537408)(./images/5001617203772_.pic.jpg)]

双向链表

结构:链表中的每个元素节点中除了存储自身元素外,还存储了下一个节点以及上一个节点,用来指向上一个节点和下一个节点。

  • 简单实现一个双向链表
package com.kyrie.utils.dataStructure;

/***
 * @project: interface-common
 * @package: com.kyrie.utils.dataStructure
 * @description: 双向链表
 * @author: kyrie
 * @mail: 18654169290@163.com
 * @createDate: 2021-04-01 23:11
 */
public class DoubleChainList {
    class Node {
        private Object value;   // 自身元素
        private Node preNode;   // 前一个节点地址
        private Node nextNode;  // 下一个节点地址

        /**
         * 新加一个链表节点,此时还未插入到原链表中
         */
        public Node(Object value) {
            this.value = value;
            this.preNode = null;
            this.nextNode = null;
        }
    }

    private Node head;  // 链表头部
    private Node tail;  // 链表尾部
    private int size;

    /**
     * 添加节点,默认插入到链表尾部
     */
    public void add(Object value) {
        addLast(new Node(value));
    }

    /**
     * 添加节点到任意位置
     *
     * @param index 节点所在下标
     * @param value 节点数据
     */
    public void add(int index, Object value) {
        checkValid(index);
        // 1.新增一个节点Node
        Node node = new Node(value);
        if (index == 0) {
            addFirst(node);
        } else if (index == size - 1) {
            addLast(node);
        } else {
            Node preNode = head;    // 节点插入的前一个Node
            Node nextNode = null;   // 节点插入的后一个Node
            for (int i = 0; i < index - 1; i++) {
                preNode = preNode.nextNode;
            }
            nextNode = preNode.nextNode;
            preNode.nextNode = node;
            node.preNode = preNode;
            nextNode.preNode = node;
            node.nextNode = nextNode;
        }
        size++;
    }

    /**
     * 添加节点到链表首位
     */
    public void addFirst(Object value) {
        addFirst(new Node(value));
    }

    /**
     * 添加节点到链表首位
     */
    public void addFirst(Node node) {
        // 2.判断当前链表长度
        if (size == 0) {
            this.head = node;
            this.tail = node;
        } else {
            Node tempNode = head;
            tempNode.preNode = node;
            node.nextNode = tempNode;
            head = node;
        }
        size++;
    }

    /**
     * 添加节点到链表尾部
     */
    public void addLast(Object value) {
        addLast(new Node(value));
    }

    /**
     * 添加节点到链表尾部
     */
    public void addLast(Node node) {
        // 2.判断当前链表长度
        if (size == 0) {
            // 当链表为空时,此时插入的节点即为尾部节点,同时当前链表只有一个节点,因此: 头部=尾部
            this.tail = node;
            this.head = node;
        } else {
            // 找到原来的尾部节点tempNode
            Node tempNode = this.tail;
            // 建立向上绑定关系:新node的前一个节点执行原尾部节点
            node.preNode = tempNode;
            // 建立向下绑定关系:原尾部节点的下一个节点指向新node
            tempNode.nextNode = node;
            // 更新尾部节点=新node
            this.tail = node;
        }
        size++;
    }

    /**
     * 获取链表指定下标节点的值
     *
     * @param index 节点下标
     */
    public Object get(int index) {
        checkValid(index);
        Object value;
        if (index == 0) {
            value = head.value;
        } else if (index == size - 1) {
            value = tail.value;
        } else {
            Node currentNode = head;
            for (int i = 0; i < index; i++) {
                currentNode = currentNode.nextNode;
            }
            value = currentNode.value;
        }
        return value;
    }

    /**
     * 移除指定节点
     *
     * @param index 节点下标
     */
    public void remove(int index) {
        checkValid(index);
        Node node;
        if (index == 0) {
            node = head.nextNode;
            node.preNode = null;
            head = node;
        } else if (index == size - 1) {
            node = tail.preNode;
            node.nextNode = null;
            tail = node;
        } else {
            Node preNode = head;    // 节点插入的前一个Node
            Node nextNode = null;   // 节点插入的后一个Node
            for (int i = 0; i < index - 1; i++) {
                preNode = preNode.nextNode;
            }
            nextNode = preNode.nextNode;
            preNode.nextNode = nextNode;
            nextNode.preNode = preNode;
        }
        size--;
    }

    /**
     * 检查传值是否合法
     *
     * @param index 节点下标
     */
    private void checkValid(int index) {
        if (index < 0 || index > size) {
            throw new ArrayIndexOutOfBoundsException("size: " + size + ",index: " + index);
        }
    }

    public static void main(String[] args) {
        DoubleChainList dChainList = new DoubleChainList();
        dChainList.add("123");
        dChainList.add("456");
        dChainList.add("789");
        dChainList.add("abc");
        dChainList.add("hahaha");
        dChainList.addFirst("firstNode");
        dChainList.add(3, "insertNode");
        dChainList.remove(0);
        System.out.println(dChainList.get(2));
    }
}

  • 双向链表图解
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUjTNbxE-1617371537410)(./images/1617335344024.png)]

  • Java中双向链表:LinkedList源码分析

LinkedList的双向链表结构

private static class Node<E> {
	E item;			 // 链表节点的自身元素值
	Node<E> next;	 // 链表节点的上一个节点指向
	Node<E> prev;	 // 链表节点的下一个节点指向

	Node(Node<E> prev, E element, Node<E> next) {
		this.item = element;
		this.next = next;
		this.prev = prev;
	}
}
  • LinkedList#addFirst(E e) 方法源码分析
public void addFirst(E e) {
	linkFirst(e);
}
/**
 * Links e as first element.
 */
private void linkFirst(E e) {
	// 首先获取当前在头节点的Node
	final Node<E> f = first;
	// 创建新添加的Node,因添加到头位置,所以初始化Node时,prev节点指向null,next节点指向原头节点的Node
	final Node<E> newNode = new Node<>(null, e, f);
	// 当前添加的节点自然是作为链表的头节点
	first = newNode;
	// 这里是检查原头节点是否为null,为null则说明原链表为空
	if (f == null)
		last = newNode;	// 原链表为空时,当前add的节点,即是头节点,也是尾节点,即:头部=尾部
	else
		// 原链表不为空时,此时当前add的节点已经作为头节点了,接下来只需要将双向链表的指向关系指定即ok,
		// 即:原头节点的上一个节点指向的是当前的节点【本人有个小疑问,虽然是当前节点就是头节点,指向当前节点和指向头节点实际上貌似没啥区别,那么为什么不直接指向头节点呢,,f.prev = first 】
		f.prev = newNode;
	size++;
	modCount++;
}
  • LinkedList#addLast(E e) 方法源码分析
public void addLast(E e) {
	linkLast(e);
}
/**
 * Links e as last element.
 */
void linkLast(E e) {
	// 首先获取链表尾节点Node
	final Node<E> l = last;
	// 创建新添加的Node,因添加到尾部,固初始化Node时,上一个节点指向原尾节点,下一个节点指向null
	final Node<E> newNode = new Node<>(l, e, null);
	// 新添加的Node自然作为尾节点
	last = newNode;
	// 此处判断当前链表是否为空,l 为 null即表示为空链表,即头部=尾部
	if (l == null)
		first = newNode;
	else
		// 设置链表节点的指向关系,原尾节点的下一个节点指向当前新添加的节点
		l.next = newNode;
	size++;
	modCount++;
}
  • LinkedList#add(int, E) 方法源码分析1
public void add(int index, E element) {
	// 索引检查,源码在下方贴出,比较简单,不做解释
	checkPositionIndex(index);
	// 针对链表的添加,插头和插尾属于直接插入法,效率比较高,因此,对于指定索引的add动作,优先判断是否属于头插和尾插
	if (index == size)
		linkLast(element);	// 索引index=size时,表示,在链表最后插入一个节点,直接引用addLast即可
	else
		linkBefore(element, node(index));
}
/**
 * Returns the (non-null) Node at the specified element index.
 * 获取指定索引上的Node节点
 * 使用二分法提高链表的遍历查找效率,防止全链表遍历
 */
Node<E> node(int index) {
	// assert isElementIndex(index);
	// index < (size >> 1) :使用二分法判断传入的索引在链表的大致位置,若索引小于链表长度的一半,则表示在链表的做半部分中,反之
	if (index < (size >> 1)) {
		// 索引在链表的左部分时,从链头开始遍历查找
		Node<E> x = first;
		for (int i = 0; i < index; i++)
			x = x.next;
		return x;
	} else {
		// 索引在链表的右部分时,从链尾开始遍历查找
		Node<E> x = last;
		for (int i = size - 1; i > index; i--)
			x = x.prev;
		return x;
	}
}
/**
 * 找到原链表指定索引位置处的节点后,再将新Node插入到该节点前面
 */
void linkBefore(E e, Node<E> succ) {
	// assert succ != null;
	// 获取原链表索引位置节点的上一个节点
	final Node<E> pred = succ.prev;
	// 创建新节点,上一个节点指向原节点的上一个节点,下一个节点指向原节点
	final Node<E> newNode = new Node<>(pred, e, succ);
	// 修改原索引位置节点的指向关系,上一个节点指向当前插入的节点
	succ.prev = newNode;
	// 若原索引位置节点的上一个节点为null,则说明原索引位置为头节点,因此直接头插新节点
	if (pred == null)
		first = newNode;
	else
		// 修改原索引位置节点上一个节点的指向关系,其下一个节点指向当前插入的节点
		pred.next = newNode;
	size++;
	modCount++;
}

private void checkPositionIndex(int index) {
	if (!isPositionIndex(index))
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
	return index >= 0 && index <= size;
}
  • LinkedList#getFirst(int, E) 方法源码分析
/**
 * 获取链表头节点的值
 */
public E getFirst() {
	final Node<E> f = first;
	if (f == null)
		throw new NoSuchElementException();
	return f.item;
}
  • LinkedList#getLast(int, E) 方法源码分析
/**
 * 获取链表尾节点的值
 */
public E getLast() {
	final Node<E> l = last;
	if (l == null)
		throw new NoSuchElementException();
	return l.item;
}
  • LinkedList#get(int index) 方法源码分析
/**
 * 获取链表指定索引位置节点的值
 */
public E get(int index) {
	// 检查插入的链表索引是否越界
	checkElementIndex(index);
	return node(index).item;
}
private void checkElementIndex(int index) {
	if (!isElementIndex(index))
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
	return index >= 0 && index < size;
}
  • LinkedList#set(int index, E element) 方法源码分析
/**
 * 设置链表指定索引位置节点的值
 * 设置新值后返回旧值
 */
public E set(int index, E element) {
	checkElementIndex(index);
	Node<E> x = node(index);
	E oldVal = x.item;
	x.item = element;
	return oldVal;
}
  • LinkedList#removeFirst() 方法源码分析
/**
 * 移除链表表头节点
 */
public E removeFirst() {
	// 获取头节点Node
	final Node<E> f = first;
	if (f == null)
		throw new NoSuchElementException();
	return unlinkFirst(f);
}
/**
 * Unlinks non-null first node f.
 * 移除头部节点后,返回头部节点的元素值element
 */
private E unlinkFirst(Node<E> f) {
	// assert f == first && f != null;
	// 获取头节点的元素值
	final E element = f.item;
	// 获取头节点的下一个节点Node
	final Node<E> next = f.next;
	// 清空头节点的元素及节点指向关系【协助JVM垃圾回收】
	f.item = null;
	f.next = null; // help GC
	// 将第二个节点设置为头节点
	first = next;
	// 此处判断链表是否只包含一个节点,若只包含一个节点,则移除头节点后,头部=尾部=null
	if (next == null)
		last = null;
	else
		// 设置第二个节点的前一个节点为null
		next.prev = null;
	size--;
	modCount++;
	return element;
}
  • LinkedList#removeLast() 方法源码分析
/**
 * 移除链表表头节点
 */
public E removeLast() {
	final Node<E> l = last;
	if (l == null)
		throw new NoSuchElementException();
	return unlinkLast(l);
}
/**
 * Unlinks non-null last node l.
 * 移除尾部节点后,返回尾部节点的元素值element
 */
private E unlinkLast(Node<E> l) {
	// assert l == last && l != null;
	// 获取链表尾节点元素值
	final E element = l.item;
	// 获取链表尾节点的前一个节点Node
	final Node<E> prev = l.prev;
	// 清空尾节点的值及引用关系【JVM垃圾回收】
	l.item = null;
	l.prev = null; // help GC
	// 尾节点指向倒数第二个节点
	last = prev;
	// 此处判断链表是否只存在一个节点,若是,则移除节点后: 头部=尾部=null
	if (prev == null)
		first = null;
	else
		// 原链表倒数第二个节点成为尾节点后,设置下一个节点的引用为null
		prev.next = null;
	size--;
	modCount++;
	return element;
}
  • LinkedList#remove(int index)方法源码分析
/**
 * 移除链表指定索引位置的节点
 */
public E remove(int index) {
	checkElementIndex(index);
	return unlink(node(index));
}
/**
 * Unlinks non-null node x.
 * 移除指定节点,并返回所移除节点的元素值
 */
E unlink(Node<E> x) {
	// assert x != null;
	// 获取当前移除节点的元素值
	final E element = x.item;
	// 获取当前移除节点的前一个节点和后一个节点
	final Node<E> next = x.next;
	final Node<E> prev = x.prev;
	
	if (prev == null) {
		// 若前一个节点为null,则表示当前移除的节点为头节点,头节点直接指向下一个节点
		first = next;
	} else {
		// 若前一个节点不为null ,则设置prev节点的下一个节点直接引用到next节点,并移除当前节点对前一个节点的引用关系【清空移除节点的引用关系】
		prev.next = next;
		x.prev = null;
	}

	if (next == null) {
		// 若next节点为null,则表示当前移除的节点为尾节点,即直接设置尾节点指向prev节点
		last = prev;
	} else {
		// 若next节点不为null,则修改next节点前一个节点的引用关系为prev节点,同时移除当前节点对下一个节点的引用关系【清空移除节点的引用关系】
		next.prev = prev;
		x.next = null;
	}
	// 清空节点元素【上面移除了当前节点的引用关系,这里再移除元素值,JVM就可以直接进行垃圾回收了】
	x.item = null;
	size--;
	modCount++;
	return element;
}
  • LinkedList#remove()方法源码分析
/**
 * 该方法直接引用removeFirst()方法,默认移除头节点
 */
public E remove() {
	return removeFirst();
}
  • LinkedList#remove(Object o)方法源码分析
/**
 * 从此列表中删除指定元素的第一个匹配项(如果存在)。
 * 如果此列表不包含该元素,则它将保持不变。
 * 更正式地说,删除索引最低的元素
 * 【移除元素需要遍历整个列表】
 */
public boolean remove(Object o) {
	if (o == null) {
		// 从头节点开始遍历,移除链表中元素为null的第一个匹配节点,若没有匹配,则链表保持不变
		for (Node<E> x = first; x != null; x = x.next) {
			if (x.item == null) {
				unlink(x);
				return true;
			}
		}
	} else {
		// 从头节点开始遍历,移除链表中指定元素的第一个匹配节点,若没有匹配,则链表保持不变
		for (Node<E> x = first; x != null; x = x.next) {
			if (o.equals(x.item)) {
				unlink(x);
				return true;
			}
		}
	}
	return false;
}

另外LinkedList中还有以下常用的方法,有兴趣可自行查看源码:

  • LinkedList#indexOf(Object o) 从表头开始检索链表中指定元素第一次出现的索引
  • LinkedList#lastIndexOf(Object o) 从表尾开始检索链表中指定元素第一次出现的索引
  • LinkedList#peek() 检索链表表头节点中的元素值,链表为空会返回null
  • LinkedList#peekFirst() 检索链表表头节点中的元素值,链表为空会返回null
  • LinkedList#peekLast() 检索链表表尾节点中的元素值,链表为空会返回null
  • LinkedList#element() 检索链表表头节点中的元素值,链表为空会抛出NoSuchElementException异常
  • LinkedList#poll() 检索链表表头节点的元素值并移除表头【弹出表头】,链表为空则返回null
  • LinkedList#pollFirst() 检索链表表头节点的元素值并移除表头【弹出表头】,链表为空则返回null
  • LinkedList#pollLast() 检索链表表尾节点的元素值并移除表头【弹出表尾】,链表为空则返回null
  • LinkedList#offer(E e) 添加指定的元素作为此链表的尾部(最后一个元素)
  • LinkedList#offerFirst(E e) 在当前链表头部插入指定元素
  • LinkedList#offerLast(E e) 在当前链表尾部插入指定元素

循环链表

结构:链表中的每个元素节点中除了存储自身元素外,还存储了下一个节点以及上一个节点,用来指向上一个节点和下一个节点。且尾节点的下一个节点【tail】指向了头节点【head】,头节点的上一个节点指向的是尾节点。循环链表分为循环单链表和循环双链表,实际原理与上述的单向链表和双向链表很类似,只是多了一层头部和尾部引用关系,可依据上述单向链表和双向链表代码进行变式实现循环链表,此处不做过多的说明,有兴趣可自行实现。


  1. 因 LinkedList#add(E) 方法内部调用的也是 addLast 方法,固不再做源码分析,参考addLast源码分析即可。 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值