一. 什么是链表
我们以前博文中已经实现的线性数据结构:
链表和之前的数据结构不同, 它是真正的动态数据结构
1.链表的特点:
- 真正的动态数据结构
- 最简单的动态数据结构
- 可以更深入的理解引用(或指针)
- 可以更深入的理解递归
- 辅助组成其他数据结构
2.链表的描述
- 数据存储在“节点”(Node)中
- 优点:真正的动态,不需要处理固定容量的问题
- 缺点:丧失了随机访问的能力(无法索引)
3. 数组和链表的对比
- 数组
- 最好用于索引有语义的情况
- 最大优点:支持快速查询
- 链表
- 不适合索引有语义的情况
- 最大优点:动态
4.代码架构搭建
新建项目LinkedList, src中新建文件LinkedList.java:
public class LinkedList<E> {
private class Node{
public E e;
public Node next;
public Node(E e, Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e, null);
}
public Node(){
this(null, null);
}
@Override
public String toString(){
return e.toString();
}
}
}
二. 在链表中添加元素
1.在表头添加元素
2.在中间插入元素
顺序很重要,如果顺序颠倒就出错, 必须
先 node.next = prev.next
后 prev.next = node
代码实现
LinkedList.java
public class LinkedList<E> {
private class Node {···}
private Node head;
int size;
public LinkedList() {
head = null;
size = 0;
}
//获取链表中的元素个数
public int getSize() {
return size;
}
// 返回链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 在链表头添加新的元素e
public void addFirst(E e) {
// Node node = new Node(e);
// node.next = head;
// head = node
head = new Node(e, head); //一句话代表了三句话的意思
size++;
}
// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作, 练习用
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index");
}
if (index == 0) { //index=0时需要特殊处理
addFirst(e);
} else {
Node pre = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next = new Node(e, prev.next);
size++;
}
}
// 在链表末尾添加新的元素
public void addLast(E e){
add(size, e);
}
}
三. 使用链表的虚拟头节点(dummyHead)
为了逻辑上的方便,设立虚拟头节点dummyHead.
这样所有的节点都有前节点了。
修改add方法:
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index");
}
// 不需要对index=0的情况特殊处理了
Node prev = dummyHead;
for (int i = 0; i < index; i++) { //i<index-1改为i<index
prev = prev.next;
}
// Node node = new Node(e);
// node.next = prev.next;
// prev.next = node;
prev.next = new Node(e, prev.next);
size++;
}
// 在链表头添加新的元素e
public void addFirst(E e) {
// head = new Node(e, head);
// size++;
add(0, e);
}
// 在链表末尾添加新的元素
public void addLast(E e){
add(size, e);
}
四. 链表的遍历,查询和修改
在LinkedList中编写相关代码
//获取链表的第index(0-based)个位置元素
public E get(int index){
if(index < 0 || index>=size){
throw new IllegalArgumentException("Get failed. Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0; i < index; i++){
cur = cur.next;
}
return cur.e;
}
public E getFirst(){
return get(0);
}
public E getLast(){
return get(size-1);
}
//修改链表第index个位置的元素
public void set(int index, E e){
if(index < 0 || index>=size){
throw new IllegalArgumentException("Set failed. Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0; i < index; i++){
cur = cur.next;
}
cur.e = e;
}
// 查找链表中是否有元素e
public boolean contains(E e){
Node cur = dummyHead.next;
while(cur != null){ //cur!=null表示cur为有效节点
if(cur.e.equals(e)){
return true;
}
cur = cur.next;
}
return false;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while(cur != null){
res.append(cur+"->");
cur = cur.next;
}
res.append("null");
return res.toString();
}
在Main.java中编写对应的测试代码
public class Main {
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, 666);
System.out.println(linkedList);
}
}
运行结果:
0->null
1->0->null
2->1->0->null
3->2->1->0->null
4->3->2->1->0->null
4->3->666->2->1->0->null
五. 从链表中删除元素
1. 原理:
2. 在LinkedList中编写删除操作的代码
// 从链表删除元素,返回被删除的元素
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed.Illegal index.");
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node delnode = prev.next;
prev.next = delnode.next;
delnode.next = null;
size--;
return delnode.e;
}
// 从链表中删除第一个元素, 返回被删除元素
public E removeFirst() {
return remove(0);
}
// 从链表中删除最后一个元素, 返回被删除元素
public E removeLast() {
return remove(size - 1);
}
3. 链表的时间复杂度分析
- 添加操作O(n)
addLast(e) O(n)
addFirst(e) O(1)
add(index, e) O(n/2) = O(n)
- 删除操作O(n)
removeLast(e) O(n)
removeFirst(e) O(1)
remove(index, e) O(n/2) = O(n)
- 修改操作O(n)
set(index, e) O(n)
- 查找操作O(n)
get(index) O(n)
contains(e) O(n)
得出结论
如果只对链表头进行操作:O(1)
只查链表头的元素:O(1)
因为这样的特性,我们可以利用链表实现栈
六. 使用链表实现栈
接口文件Stack.java
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
基于链表实现栈LinkedListStack.java
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> list;
public LinkedListStack(){
list = new LinkedList<>();
}
@Override
public int getSize(){
return list.getSize();
}
@Override
public boolean isEmpty(){
return list.isEmpty();
}
@Override
public void push(E e){
list.addFirst(e);
}
@Override
public E pop(){
return list.removeFirst();
}
@Override
public E peek(){
return list.getFirst();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack: top ");
res.append(list);
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);
}
}
运行结果:
Stack: top 0->null
Stack: top 1->0->null
Stack: top 2->1->0->null
Stack: top 3->2->1->0->null
Stack: top 4->3->2->1->0->null
Stack: top 3->2->1->0->null
对ArrayStack和LinkedListStack进行效率对比
Main.java
import java.util.Random;
public class Main {
private static double testQueue(Stack<Integer> q, int opCount) {
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < opCount; i++) {
q.push(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i < opCount; i++) {
q.pop();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0; // 纳秒转为秒
}
public static void main(String[] args) {
int opCount = 100000;
// ArrayStack在resize中消耗很多时间
ArrayStack<Integer> arraystack = new ArrayStack<>();
double time1 = testQueue(arraystack, opCount);
System.out.println("ArrayStack, time: " + time1);
// LinkedListStack在不停地new Node的过程中消耗很多时间
LinkedListStack<Integer> linkedliststack = new LinkedListStack<>();
double time2 = testQueue(linkedliststack, opCount);
System.out.println("LinkedListStack, time: " + time2);
}
}
运行结果
ArrayStack, time: 0.032472117
LinkedListStack, time: 0.02170341
将opCount变为10000000
ArrayStack, time: 1.48001151
LinkedListStack, time: 3.457722535
- ArrayStack在resize中消耗很多时间
- LinkedListStack在不停地new Node的过程中消耗很多时间
- 因为时间复杂度相同, 所以两者效率差不多。
七. 带有尾指针的链表: 使用链表实现队列
1. 改进我们的链表
2. 代码实现
在队列的项目Queue中新建LinkedListQueue.java
public class LinkedListQueue<E> implements Queue<E> {
private class Node {
private E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
private Node head, tail;
private int size;
public LinkedListQueue() {
head = null;
tail = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(E e) {
if (tail == null) {
tail = new Node(e);
head = tail;
} else {
tail.next = new Node(e);
tail = tail.next;
}
size++;
}
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
}
Node retNode = head;
head = head.next;
retNode.next = null;
if (head == null) {
tail = null;
}
size--;
return retNode.e;
}
@Override
public E getFront(){
if(isEmpty()){
throw new IllegalArgumentException("Queue is empty");
}
return head.e;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue front ");
Node cur = head;
while(cur != null){
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL tail");
return res.toString();
}
//测试
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
if (i % 3 == 2) {
queue.dequeue();
System.out.println(queue);
}
}
}
}
运行得到测试结果:
Queue front 0->NULL tail
Queue front 0->1->NULL tail
Queue front 0->1->2->NULL tail
Queue front 1->2->NULL tail
Queue front 1->2->3->NULL tail
Queue front 1->2->3->4->NULL tail
Queue front 1->2->3->4->5->NULL tail
Queue front 2->3->4->5->NULL tail
Queue front 2->3->4->5->6->NULL tail
Queue front 2->3->4->5->6->7->NULL tail
Queue front 2->3->4->5->6->7->8->NULL tail
Queue front 3->4->5->6->7->8->NULL tail
Queue front 3->4->5->6->7->8->9->NULL tail
3.在Main.java中比较一下三种Queue(ArrayQueue, LoopQueue, LinkedListQueue)的效率:
Main.java
import java.util.Random;
public class Main {
private static double testQueue(Queue<Integer> q, int opCount) {
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < opCount; i++) {
q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i < opCount; i++) {
q.dequeue();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0; // 纳秒转为秒
}
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue, opCount);
System.out.println("ArrayQueue, time: "+time1);
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue, time: "+time2);
LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();
double time3 = testQueue(linkedListQueue, opCount);
System.out.println("LinkedListQueue, time: "+time3);
}
}
运行结果:
ArrayQueue, time: 43.617662698
LoopQueue, time: 0.019108906
LinkedListQueue, time: 0.010479048