数据结构梳理(2) - 线性表的链式表示之链表

本文深入解析链表数据结构,涵盖概念、基本操作、优缺点及与数组对比,附带单向与双向链表的完整代码实现,助你掌握链表的核心知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

发一下牢骚,本来这个数据结构梳理的系列是在我找工作之前开始的,但是在中间找工作的过程中,一部分原因是面试太忙时间比较少,只能舍重就轻,当然更大的原因还是我自己的惰性,为了保持这个系列的完整性,以及自己日后的复习,于是决定重新开始,按照之前的思路,将这个系列梳理完,中间也会穿插一些关于这些数据结构在面试中的考题。

好了,上一篇讲的最基础的线性表的顺序表示,也就是基于数组,那么本篇的主要内容就是线性表的链式表示,简言之就是链表

目录

1、链表的概念
2、链表的基本操作
3、链表的优缺点
4、链表和数组的对比
5、单(双)向链表的完整代码

正文

1、链表的概念

用一组任意的存储单元存储线性表的数据元素的的数据结构。

很简单,值得一说的是,这个任意的存储单元,既可以是连续的,也可以是不连续的,在这个概念中的“单元”这个词,其实就是我们所熟知的节点,每个节点包含两个域,一个数据域,一个指针域或者两个指针域,如果是一个指针域,就是单向链表,如果是两个指针域,就是双向链表。

2、链表的基本操作

因为这里只是基本操作,所以就拿单向链表来举例子,双向链表只是比单向链表多了个指针域,只要把单向链表弄明白了,对应双向链表的基本操作就自然而然会了。

a.初始化
上一节说线性表的顺序表示时,也说过初始化,这里的初始化也大同小异,主要包括这样一些值的确定,一个是初始容量,另一个就是空节点的值。

空节点的值可以使用int类型的默认值,也就是0,因为是链表,所以初始容量一般是0,还记得线性表初始化时的初始容量和负载因子这两个概念吗,这里之所以不需要这两个东东,是因为我们在声明一个数组的时候,必须要给它一个初始容量,而链表就比较灵活了,需要一个元素直接链上去就行。

由于是链表,另一个需要初始化确定的就是节点,因为这里以单向链表来说明,所以每个节点一个数据成员变量,一个next指针成员变量,由于内部类的特性,所以节点类一般声明为内部类,如下

	public class Node {

		private int data;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}

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

	}

然后是单向链表类的初始化,按照上面描述的,如下

	private Node head;//头结点
	private Node tail;//尾节点
	private int size = 0;//链表长度

	public SingleLinkList() {
		head = null;
		tail = null;
		size = 0;

	}
	
	public SingleLinkList(int data) {
		head = new Node(data, null);
		tail = head;
		size++;
	}

为了使用的方便,这里额外加了个使用一个元素值初始化链表的构造方法,不过这个不重要,看使用的需求即可,也可为了方便,自己加上两个、三个等元素的构造方法来初始化。

b.增加元素

增加元素为了增加的效率,这里分为三种情况,增加在链表头的位置,增加在链表尾的位置,增加在链表中间的位置。所以我们可以写三个对应的方法。其中相对复杂点的就是增加在链表中间的位置,对应的方法代码如下

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);//找到前一个节点
				preNode.next = new Node(data, preNode.next);
				size++;
			}
		}
	}

如果不是自己动手写的话,还是会发现即便只是一个增加代码,还是要考虑很多细节问题的,比如,一开始判断插入位置的合法性,以及链表的判空,这样就可以直接使用尾插来增加元素了,如果查找到应插在链表头,则使用头插来插入这个元素,否则,我们才手动进行元素的插入。因为我使用了Node的构造方法,所以这里我只用了一行代码,其实也是最核心的代码,大致分为两步,第一步,将待插入节点的next指针指向插入位置的next指针指向的节点,第二步,将前一个节点的next指针指向待插入节点。

然后再贴上头插和尾插的方法代码,头插的代码如下

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, head);
			head = newNode;
		}
		size++;
	}

尾插的代码如下

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null);
			tail.next = newNode;
			// 将尾指针指向最新的最后一个节点
			tail = newNode;
		}
		size++;
	}

c.删除元素
删除元素,为了使用的方便性,也写了两个方法,一个是根据下标来删除,也就是从链表头开始的小标,另一种是根据元素值来删除。

首先我们来看根据下标来删除元素,代码如下

	// 删除指定位置的节点
	public void deleteByIndex(int index) {
		if (head == null) {
			System.out.println("链表为空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 删除头节点
			{
				deleteNode = head;
				head = head.next;
				size--;
			} else if (index == size - 1) {// 删除尾节点
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
				deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
				prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
				size--;
			}
			deleteNode = null;
		}
	}

同样的,核心操作也只有三步,代码注释写的很清楚所以就不赘述了,但是为了健壮性和效率性,我们要尽可能的考虑到所有的情况,来优化我们的代码。

接下来是根据元素值来定向删除,代码如下

	// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (index == -1) {
			System.out.println("链表为空");
		} else if (index == -2) {
			System.out.println("未找到对应的元素");
		} else {
			deleteByIndex(index);
		}
	}

其核心就是调用了一个查找方法和上面的删除方法,查找方法后面会说到,先不急,我们主要是思路。当然为了效率, 我们也可以写一个尾删的方法,三行搞定

	// 删除 链表中最后一个元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

所以我们只要弄懂了最核心的方法,其它的我们都可以去利用这个方法自由扩展。

d.查找元素
好了,到了最后一个基本操作,就是查找元素。同样的,根据我们平时使用的需求,主要是分为两种,根据下标查找对应的值,以及已知值查找对应的下标。

我们首先看第一种查找,根据下标查找对应的值,代码如下

	// 通过index查找指定的节点
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

首先是一连串的判断,然后来到一个核心的循环,由于是单向链表,所以只能从链表头开始一个个去遍历。弄明白了这个方法,那么第二种查找,根据值来查找对应的下标也就是小case了。代码如下

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 数组为空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 从第一个节点开始查找对比数据
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到对应的节点
	}

要多说一下的是,如果值没有找到,我这里是返回的负数,来代表不同的含义,当然在实际中,最好是抛出异常处理,这样便于代码的异常排查。

3、链表的优缺点

在熟悉了链表的基本操作之后,我们可以很明显的发现一个问题,就是链表的插入和删除非常简单,但是链表的遍历就有点麻烦了,总感觉没有数组方便,数组直接定位,真快,但是链表还得从头开始去遍历,所以对于链表来说,优点是插入和删除效率高,但是查找效率低,相应的,链表适用于大量插入删除的场景,而对于一些需要频繁查找的场景就不那么适合了。

4、链表和数组的对比与合体

分析了一波链表的优缺点之后,我们再回想下数组,发现数组的优缺点和链表是正好反着的,数组是适用于查找,但是插入删除效率低,因为涉及到元素大量移动的问题,而链表正好弥补了数组的缺点,但是它却在查找方面表现不佳。

所以没有哪一种是完美的,或者说通用的,这二者的取舍需要我们根据实际的使用场景来自行选择。

那么我们是否想过这样一个问题,既然这两个互补,那我们能不能有一种东西把这两个对象的优点综合起来呢,这岂不是很完美了,对吧,当然,Java的设计者当然考虑过这个问题,于是他们设计了一个容器叫做HashMap,嘿嘿嘿,想了解它的原理吗?想了解其中的奥秘吗?想知道优秀的Java设计者是如何综合数组和链表的优势的吗?限于篇幅,我就不详解了,这里我就丢下三个字:哈希表。剩下的就是你们自己去研究啦,hhhh…

5、单(双)向链表的完整代码

单向链表的完整代码如下

public class SingleLinkList {

	public class Node {

		private int data;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}

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

	}

	private Node head;
	private Node tail;
	private int size = 0;

	public SingleLinkList() {
		head = null;
		tail = null;
		size = 0;

	}

	public Node getHead() {
		return head;
	}

	public Node getTail() {
		return tail;
	}

	public SingleLinkList(int data) {
		head = new Node(data, null);
		tail = head;
		size++;
	}

	public int getLength() {
		return size;
	}

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, head);
			head = newNode;
		}
		size++;
	}

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null);
			tail.next = newNode;
			// 将尾指针指向最新的最后一个节点
			tail = newNode;
		}
		size++;
	}

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);
				preNode.next = new Node(data, preNode.next);
				size++;
			}
		}
	}

	// 通过index查找指定的节点
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 数组为空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 从第一个节点开始查找对比数据
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到对应的节点
	}

	// 删除指定位置的节点
	public void deleteByIndex(int index) {
		if (head == null) {
			System.out.println("链表为空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 删除头节点
			{
				deleteNode = head;
				head = head.next;
				size--;
			} else if (index == size - 1) {// 删除尾节点
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
				deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
				prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
				size--;
			}
			deleteNode = null;
		}
	}

	// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (index == -1) {
			System.out.println("链表为空");
		} else if (index == -2) {
			System.out.println("未找到对应的元素");
		} else {
			deleteByIndex(index);
		}
	}

	// 删除 链表中最后一个元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

	// 更新指定位置的值
	public void updateByIndex(int data, int index) {
		findNodeByIndex(index).data = data;
	}

	// 清除链表中所有的元素
	public void clear() {
		head = null;
		tail = null;
		size = 0;
	}

	// 判断链表是否为空
	public boolean isEmpty() {
		return size == 0;
	}

	// 链表的输出 重写toString方法
	public String toString() {
		if (isEmpty()) {
			return "null";
		} else {
			StringBuilder sb = new StringBuilder("");
			for (Node current = head; current != null; current = current.next)// 从head开始遍历
			{
				sb.append(current.data + "-");
			}
			int len = sb.length();
			return sb.delete(len - 1, len).append("").toString();// 删除最后一个 -
		}
	}
	
	public static void main(String[] args) {		
		SingleLinkList list = new SingleLinkList();

		list.addAtHead(1);
		list.addAtHead(2);
		list.addAtHead(3);
		list.addAtTail(4);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.addAtIndex(9, 3);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteByIndex(4);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteByData(2);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteLast();
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());
		
		list.addAtTail(5);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.addAtTail(5);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.updateByIndex(66, 3);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());
		
		list.clear();
		System.out.println(list+"  size="+list.getLength());

	}
}

同理,我们可以稍加扩展,写出双向链表的代码,如下

public class DoubleLinkList {

	public class Node {
		private int data;
		private Node pre;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}
		
		public Node getPre() {
			return pre;
		}

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

	private Node head;
	private Node tail;
	private int size = 0;

	public DoubleLinkList() {
		head = null;
		tail = null;
		size = 0;
	}

	public Node getHead() {
		return head;
	}

	public Node getTail() {
		return tail;
	}

	public DoubleLinkList(int data) {
		head = new Node(data, null, null);
		tail = head;
		size++;
	}

	public int getLength() {
		return size;
	}

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null, head);
			head = newNode;
		}
		size++;
	}

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null, null);
			tail = head;
		} else {
			Node newNode = new Node(data, tail, null);
			tail.next = newNode;
			// 将尾指针指向最新的最后一个节点
			tail = newNode;
		}
		size++;
	}

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);
				Node addNode = new Node(data, preNode, preNode.next);
				preNode.next.pre = addNode;// 先设置后一个节点的前驱
				preNode.next = addNode;// 再设置前一个节点的后继
				size++;
			}
		}
	}

	// 通过index查找指定的节点
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 数组为空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 从第一个节点开始查找对比数据
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到对应的节点
	}

	// 删除指定位置的节点
	public void deleteByIndex(int index) {
		if (isEmpty()) {
			throw new RuntimeException("链表为空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 删除头节点
			{
				Node current = head.next;
				current.pre = null;
				deleteNode = head;
				head = current;
				size--;
			} else if (index == size - 1) {// 删除尾节点
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
				deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
				prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
				deleteNode.next.pre = prevNode;// 设置删除节点的后继节点的前驱等于 删除节点的前驱
				size--;
			}
			deleteNode = null;
		}
	}

	// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (isEmpty()) {
			throw new RuntimeException("链表为空");
		} else if (index == -2) {
			System.out.println("未找到对应的元素");
		} else {
			deleteByIndex(index);
		}
	}

	// 删除 链表中最后一个元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

	// 更新指定位置的值
	public void updateByIndex(int data, int index) {
		findNodeByIndex(index).data = data;
	}

	// 判断链表是否为空
	public boolean isEmpty() {
		return size == 0;
	}

	// 清除链表中所有的元素
	public void clear() {
		head = null;
		tail = null;
		size = 0;
	}

	// 链表的输出 重写toString方法
	public String toString() {
		if (isEmpty()) {
			return "null";
		} else {
			StringBuilder sb = new StringBuilder("");
			for (Node current = head; current != null; current = current.next)// 从head开始遍历
			{
				sb.append(current.data + "-");
			}
			int len = sb.length();
			return sb.delete(len - 1, len).append("").toString();// 删除最后一个 -
		}
	}
	
	public static void main(String[] args) {
		DoubleLinkList list = new DoubleLinkList();
		list.addAtHead(1);
		list.addAtHead(2);
		list.addAtHead(3);
		list.addAtTail(4);
		list.addAtTail(5);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.addAtIndex(9, 0);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteByIndex(2);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteByData(4);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteLast();
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(55, 1);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(44, 0);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(66, 2);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());


	}
}

结语

好了,本篇就到此为止了,虽然内容有点多,但是原理都比较简单,主要是要自己动手,基本上手动实现一遍就差不多了,按照计划,下一篇应该是栈的梳理,下一篇见!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值