数据结构与算法之单链表

(一) 单链表

1.定义

链表是一种物理存储结构上非连续,非顺序的线性数据存储结构,数据元素的逻辑顺序是通过链表中的节点(Node)链接次序实现的。每个链表中都包含两部分: 存储的数据 和 下一个节点(Node).

class Node{
	private E e; // 存储的数据
	private Node next; // 下一个节点
}

在之前的博客中实现的动态数组、栈以及队列时,底层都是依托的静态数组,靠resize来解决固定容量的问题, 而链表是真正的动态数据结构, 无需处理固定容量问题.

2. 图解
  • 数据存储在节点(Node)中, 当一个节点(next)为NULL时, 代表此元素为链表的最后一个元素
    在这里插入图片描述
  • 链表插入元素
    在这里插入图片描述
  • 链表删除元素
    -在这里插入图片描述
3.数组和链表的对比
  • 数组静态分配内存, 链表动态分配内存.
  • 数组在内存中是连续的,链表是不连续的。
  • 数组利用索引定位,查找的时间复杂度是O(1),链表通过遍历定位元素,查找的时间复杂度是O(N)。
  • 数组插入和删除需要移动其他元素,时间复杂度是O(N),链表的插入或删除不需要移动其他元素,时间复杂度是O(1)。

(二) 自定义链表LinkedList

1. 新建自定义链表实体类LinkedList
public class LinkedList<E> {

	/**
	 * 节点内部类: 可以单独定义出来, 但用户无需关注链表底层实现, 因此定义成LinkedList的内部类
	 * 
	 * @author Admin_Lian
	 *
	 */
	private class Node {
		/**
		 * 存储的数据
		 */
		public E e;
		
		/**
		 * 下一个节点
		 */
		public Node next;
		
		public Node() {
			this(null, null);
		}
		
		public Node(E e) {
			this(e, null);
		}
		
		public Node(E e, Node next) {
			this.e = e;
			this.next = next;
		}
		
		@Override
		public String toString() {
			return e.toString();
		}
	}
	
	/**
	 * 头部节点: 链表第一元素 
	 */
	private Node head;
	
	/**
	 * 链表元素个数
	 */
	private int size;
	
	public LinkedList() {
		head = null;
		size = 0;
	}
	
	/**
	 * 获取链表中元素的个数
	 * 
	 * @return
	 */
	public int getSize() {
		return size;
	}
	
	/**
	 * 返回链表是否为空
	 * 
	 * @return
	 */
	public boolean isEmpty() {
		return size == 0;
	}
}
2. 自定义链表的增加操作
  • 在链表头部添加新元素e
public void addFirst(E e) {
	// 创建一个新头节点
	Node headNode = new Node(e);
	// 新头节点的下一节点next 等于 当前链表的头节点
	headNode.next = this.head;
	// 当前链表的头节点 等于 新头节点
	this.head = headNode;
	
	// 第二种写法
	// this.head = new Node(e, this.head);
	size++;
}
  • 在链表index位置处添加新元素e
public void add(int index, E e) {
	if(index < 0 || index > size) {
		throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
	}
	// index == 0, 相当于在链表头部添加新元素, 但index == 0 没有上一个节点. 因此需要单独处理(待优化)
	if (index == 0) {
		addFirst(e);
	} else {
		// 查询要添加元素的前一个节点, 因此查询index-1位置处的节点
		Node prev = this.head;
		for (int i = 0; i < index - 1; i++) {
			prev = prev.next;
		}
		Node node = new Node(e);
		// 新添加的节点next 指向 index-1位置处的节点
		node.next = prev.next;
		// index-1位置处的节点next 指向 新添加的节点
		prev.next = node;
		
		// prev.next = new Node(e, prev.next);
		size++;
	}
}
  • 在链表末尾添加新元素e
public void addLast(E e) {
	add(size, e);
}
3. 为自定义链表设置一个虚拟的节点
  • 为链表设置一个虚拟的节点: 在add(idnex, e)操作, 也能找到上一个节点, 无需多余的判断. 初始链表时dummyHead, 存储的数据为Null, 下一个节点next也为Null.
    在这里插入图片描述
    优化自定义链表LinkedList
/**
 * 虚拟头部节点: 存储的数据为Null, 下一个节点next也为Null
 */
private Node dummyHead;

/**
 * 链表元素个数
 */
private int size;

public LinkedList() {
	dummyHead = new Node(null, null);
	size = 0;
}


// ....


/**
*  在链表index位置处添加新元素e: 新添加一个虚拟节点, 因此index=0时, 也能找到上一个节点
 * 
 * @param index
 * @param e
 */
public void add(int index, E e) {
	if (index < 0 || index > size) {
		throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
	}
	
	// 查询要添加元素的前一个节点, 因为新添加一个虚拟节点, 所以查询index位置处的节点
	Node prev = this.dummyHead;
	for (int i = 0; i < index; i++) {
		prev = prev.next;
	}
	Node node = new Node(e);
	// 新添加的节点next 指向 index-1位置处的节点
	node.next = prev.next;
	// index-1位置处的节点next 指向 新添加的节点
	prev.next = node;
	
	// prev.next = new Node(e, prev.next);
	size++;
}

/**
 * 在链表头部添加新元素e
 * 
 * @param e
 */
public void addFirst(E e) {
	add(0, e);
}
4. 自定义链表的查询操作
  • 获取链表的第index位置处的元素
publicE get(int index) {
	if (isEmpty()) {
		throw new IllegalArgumentException("add(index, e) failed. LinkedList is empty.");
	}
	if (index < 0 || index > size) {
		throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
	}
	// 获取当前真正第一个元素的节点
	Node cur = this.dummyHead.next;
	for (int i = 0; i < index; i++) {
		cur = cur.next;
	}
	return cur.e;
}
  • 获取链表第一个元素 和 获取链表最后一个元素
/**
 * 获取链表第一个元素
 * @return
 */
public E getFirst() {
	return get(0);
}

/**
 * 获取链表最后一个元素
 * 
 * @return
 */
public E getLast() {
	return get(size - 1);
}
5. 自定义链表的修改操作
  • 修改链表第index位置的元素为e
public void set(int index, E e) {
	if (index < 0 || index > size) {
		throw new IllegalArgumentException("add(index, e) failed. Illegal index.");
	}
	Node cur = this.dummyHead.next;
	for (int i = 0; i < index; i++) {
		cur = cur.next;
	}
	cur.e = e;
}
  • 删除链表第index位置的元素
public E remove(int index) {
	if (index < 0 || index > size) {
		throw new IllegalArgumentException("remove(index, e) failed. Illegal index.");
	}

	// 查找删除元素的上一个节点
	Node prev = this.dummyHead;
	for (int i = 0; i < index; i++) {
		prev = prev.next;
	}

	// index处的节点
	Node resNode = prev.next;

	// 上一个节点的next 等于 index处的节点next
	prev.next = resNode.next;
	resNode.next = null;
	size--;
	return resNode.e;
}
6. 自定义链表的删除操作
  • 删除链表第一个的元素 和 删除链表最后一个的元素
/**
 * 删除链表第一个的元素
 * 
 * @return
 */
public E removeFirst() {
	return remove(0);
}

/**
 * 删除链表最后一个的元素
 * 
 * @return
 */
public E removeLast() {
	return remove(size - 1);
}
  • 查找链表种是否存在e
public boolean contains(E e) {
	Node cur = this.dummyHead.next;
	// 链表最后一个元素节点的next 一定为 null
	while(cur != null) {
		if (cur.e.equals(e)) {
			return true;
		}
		cur = cur.next;
	}
	return false;
}
  • 覆盖重写toString()方法
@Override
public String toString() {
	StringBuilder res = new StringBuilder();
	res.append("LinkedList: ");
	for (Node cur = this.dummyHead.next; cur != null; cur = cur.next) {
		res.append(cur.e + " -> ");
	}
	res.append("NULL");
	return res.toString();
}

测试

public static void main(String[] args) {
	LinkedList<Integer> linkedList = new LinkedList<>();
	for (int i = 0; i < 5; i++) {
		linkedList.addFirst(i);
		System.out.println(linkedList);
	}
	
	linkedList.add(2, -1);
	System.out.println(linkedList);
	
	linkedList.set(2, -2);
	System.out.println(linkedList);
	
	linkedList.remove(2);
	System.out.println(linkedList);
}

在这里插入图片描述

(三) 时间复杂度分析

  • 添加操作: 整体时间复杂度为 O(n)
函数时间复杂度分析
addLast(e)O(n)链表尾部添加元素, 需要遍历链表, 与数据规模呈线性关系
addFirst(e)O(1)链表头部添加, 只需要改变虚拟节点的next指向, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成
add(index, e)O(n/2) = O(n)消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略
总结:O(n)添加操作整体时间复杂度为 O(n), 分析时间复杂度一般考虑最坏的情况
  • 删除操作: 整体时间复杂度为 O(n)
函数时间复杂度分析
removeLast(e)O(n)删除链表尾部元素, 需要遍历链表, 与数据规模呈线性关系
removeFirst(e)O(1)删除链表头部元素, 只需要改变虚拟节点的next指向, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成
remove(index, e)O(n/2) = O(n)消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略
总结:O(n)删除操作整体时间复杂度为 O(n)
  • 查询操作: 复杂度为 O(n)
函数时间复杂度分析
get(index)O(n)需要遍历链表, 与数据规模呈线性关系
contain(e)O(n)需要遍历链表, 与数据规模呈线性关系
总结:O(n)复杂度为 O(n)
  • 修改操作: 时间复杂度为 O(n)
函数时间复杂度分析
set(index, e)O(n)需要遍历链表, 与数据规模呈线性关系
总结:O(n)修改操作整体时间复杂度为 O(n)

(四) 使用链表实现栈

/**
 * 自定义链表栈
 * 
 * @author Admin_Lian
 *
 * @param <E>
 */
public class LinkedListStack<E> implements Stack<E> {

	// 底层采用链表存储数据结构
	private LinkedList<E> linkedList;
	
	public LinkedListStack() {
		linkedList = new LinkedList<>();
	}
	
	/**
	 * 获取栈中的有效元素个数
	 * 
	 * @return
	 */
	@Override
	public int getSize() {
		return linkedList.getSize();
	}

	/**
	 * 返回栈中有效元素是否为空
	 * 
	 * @return
	 */
	@Override
	public boolean isEmpty() {
		return linkedList.isEmpty();
	}

	/**
	 * 向栈(栈顶)中添加元素
	 * 
	 * @param e
	 */
	@Override
	public void push(E e) {
		linkedList.addFirst(e);
	}

	/**
	 * 从栈(栈顶)中取出元素
	 * 
	 * @return
	 */
	@Override
	public E pop() {
		return linkedList.removeFirst();
	}

	/**
	 * 查看栈顶元素
	 * 
	 * @return
	 */
	@Override
	public E peek() {
		return linkedList.getFirst();
	}
	
	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("Stack: top ");
		res.append(linkedList);
		return res.toString();
	}

}

测试

public static void main(String[] args) {
	LinkedListStack<Integer> stack = new LinkedListStack<>();
	for (int i = 0; i < 5; i++) {
		stack.push(i);
		System.out.println(stack);
	}
	
	stack.pop();
	System.out.println(stack);
}

在这里插入图片描述

(五) 使用链表实现队列

借用head头指针和tail尾指针实现队列
在这里插入图片描述

/**
 * 自定义链表队列
 * 
 * @author Admin_Lian
 *
 */
public class LinkedListQueue<E> implements Queue<E>{

	/**
	 * 节点内部类: 可以单独定义出来, 但用户无需关注链表底层实现, 因此定义成LinkedList的内部类
	 * 
	 * @author Admin_Lian
	 *
	 */
	private class Node {
		/**
		 * 存储的数据
		 */
		public E e;

		/**
		 * 下一个节点
		 */
		public Node next;

		@SuppressWarnings("unused")
		public Node() {
			this(null, null);
		}

		public Node(E e) {
			this(e, null);
		}

		public Node(E e, Node next) {
			this.e = e;
			this.next = next;
		}

		@Override
		public String toString() {
			return e.toString();
		}
	}

	/**
	 *  头部节点: 链表第一个元素 
	 */
	private Node head;
	
	/**
	 * 尾部节点: 链表最后一个元素
	 */
	private Node tail;
	
	/**
	 * 链表队列元素个数
	 */
	private int size;
	
	public LinkedListQueue() {
		this.head = null;
		this.tail = null;
		this.size = 0;
	}
	
	/**
	 * 获取链表队列中元素的个数
	 * 
	 */
	@Override
	public int getSize() {
		return size;
	}

	/**
	 * 返回链表队列是否为空
	 */
	@Override
	public boolean isEmpty() {
		return size == 0;
	}
	
	/**
	 * 入队: 向链表队列(队尾)中添加元素
	 */
	@Override
	public void enqueue(E e) {
		// 链表队列为空
		if (this.tail == null) {
			// 尾节点 等于 要添加元素节点
			this.tail = new Node(e);
			// 头节点 等于 尾节点
			this.head = this.tail; 
		} else {
			// 尾部节点的next 等于 要添加元素节点
			this.tail.next = new Node(e);
			// 尾部节点 等于 尾部节点的next
			this.tail = this.tail.next;
		}
		size++;
	}

	/**
	 * 出队: 从链表队列(队首)中取出一个元素
	 * 
	 * @return
	 */
	@Override
	public E dequeue() {
		if (this.tail == null) {
			throw new IllegalArgumentException("dequeue() failed. LikedListQueue is empty."); 
		}
		Node resNode = this.head;
		// 新头节点 等于 要出队的节点的next 
		this.head = resNode.next;
		resNode.next = null;
		// 链表队列只存在一个元素, 且要出队
		if (this.head == null) {
			this.tail = null;
		}
		size--;
		return resNode.e;
	}

	/**
	 * 查看链表队列队首元素
	 * 
	 * @return
	 */
	@Override
	public E getFront() {
		if (isEmpty()) {
			throw new IllegalArgumentException("getFront() failed. LikedListQueue is empty."); 
		}
		return this.head.e;
	}
	
	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("LinkedListQueue: front ");
		
//		Node node = this.head;
//		for (int i = 0; i < size; i++) {
//			res.append(node.e + " -> ");
//			node = node.next;
//		}
		
//		Node node = this.head;
//		while (node != null) {
//			res.append(node.e + " -> ");
//			node = node.next;
//		}
		
		for (Node node = this.head; node != null; node = node.next) {
			res.append(node.e + " -> ");
		}
		
		res.append("NUll tail");
		return res.toString();
	}

}

测试

public static void main(String[] args) {
	LinkedListQueue<Integer> queue = new LinkedListQueue<>();
	// 向队列中添加元素: 0-9
	for (int i = 0; i < 10; i++) {
		queue.enqueue(i);
		System.out.println(queue);
		
		// 每添加三个元素, 移除队列队首元素
		if(i % 3 == 2) {
			queue.dequeue();
			System.out.println(queue);
		}
	}
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值