链表相比于前几个章节讲的数据结构而言,是一个真正的动态数据结构也是一个最简单的动态数据结构,我们在后面还会接触更多的动态数据结构,所以对链表有一个理解非常好的基础,就能够更加容易的学习后面更加复杂的数据结构。
首先我们了解一下什么是链表,链表将数据存储在一种单独的数据结构中,这个结构通常叫做“节点”,对于链表来说,节点通常只有两部分内容,e:存储的数据,next:链表中的节点通过next指向下一个节点,就像一个火车一样,一个节点就好比一个车厢,车厢里存储真正的数据,车厢和车厢之间还需要连接,使数据都整合在一起。
public class LinkedList<E> {
//定义成私有因为,使用者是无需关心Node的实现,只要使用即可
private class Node {
//保存的数据
private E e;
//指向下一个Node节点
private 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节点,数据为2的节点指向了数据为3的节点,如果一个节点的next指向了null,说明这个节点就是最后一个节点,这个就是链表。
知道了链表这种数据结构,自然要知道对于这个数据结构的一些操作,我们来看下链表中的增删改查吧
在链表头部添加元素
为了能够在链表头部添加元素,我们就需要先维护一个叫head的节点来作为链表的头部,往头部添加元素只需要将节点的next指向head,然后将head替换成添加到头部的node即可
public class LinkedList<E> {
//定义成私有因为,使用者是无需关心Node的实现,只需使用
private class Node {
//保存的数据
private E e;
//指向下一个Node节点
private 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;
//链表中元素的数量
private int size;
//用于初始化链表
public LinkedList() {
head=null;
size=0;
}
//获取链表中元素数量
public int getSize() {
return size;
}
//判断链表是否为空
public boolean isEmpty() {
return size==0;
}
//在链表头部添加元素
public void addFirst(E e) {
Node node = new Node(e);
node.next=head;
head = node;
size++;
}
}
在链表指定元素后面添加元素
在链表中间添加元素,我们先要把新的节点的next指向指定节点后面的一个节点( 新节点.next = 指定节点.next ),然后将指定节点的next指向新节点即可( 指定节点.next = 新节点 ),注意先后顺序
package com.datastructure;
public class LinkedList<E> {
//定义成私有因为,使用者是无需关心Node的实现,只需使用
private class Node {
//保存的数据
private E e;
//指向下一个Node节点
private 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;
//链表中元素的数量
private int size;
public LinkedList() {
head=null;
size=0;
}
//获取链表中元素数量
public int getSize() {
return size;
}
//判断链表是否为空
public boolean isEmpty() {
return size==0;
}
//在链表头部添加元素
public void addFirst(E e) {
//Node node = new Node(e);
//node.next=head;
//head = node;
//上面三句等同于下面一句
head = new Node(e,head);
size++;
}
//在链表指定元素后面添加元素
//在链表中不常使用索引,此操作练习用
public void add(int index,E e) {
//参数校验
if(index<0|| index>size)
throw new IllegalArgumentException("Add failed, Illegal index.");
if(index == 0)
addFirst(e);
Node prev = head;
//遍历出index前一个Node节点
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++;
}
}
有了在链表指定元素后面添加元素的方法后,在尾部添加方法就很简单了,只需要调用add方法index传入的参数为size即可
public void addLast(E e) {
add(size,e);
}
但是,addFirst方法的逻辑和在链表其他位置添加元素的逻辑不同,因为我们在调用add方法添加元素的时候,是通过遍历到待添加位置的前一个节点进行添加操作,由于链表头是第一个节点,没有前一个节点,也就没办法调用add方法在链表头中插入元素,为了能够让代码更加优雅,在链表中有个非常实用的方法来统一add的操作,就是在链表头部定义一个空节点作为头部节点称之为虚拟节点(dummyHead),这个虚拟节点不储存任何数据,这样一来第一个元素就是dummyHead的next所对应的元素了,addFirst方法也自然可以复用add方法了。
下面“ * ”标记了修改了的代码
public class LinkedList<E> {
//定义成私有因为,使用者是无需关心Node的实现,只需使用
private class Node {
//保存的数据
private E e;
//指向下一个Node节点
private 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 dummyHead;
//链表中元素的数量
private int size;
public LinkedList() {
* dummyHead=new Node();
size=0;
}
//获取链表中元素数量
public int getSize() {
return size;
}
//判断链表是否为空
public boolean isEmpty() {
return size==0;
}
//在链表头部添加元素
//修改之后的addFirst方法
public void addFirst(E e) {
* add(0,e);
}
//在指定位置添加元素
//在链表中不常使用索引,此操作练习用
public void add(int index,E e) {
//参数校验
if(index<0|| index>size)
throw new IllegalArgumentException("Add failed, Illegal index.");
* Node prev = dummyHead;
//遍历出index前一个Node节点
//这里把index-1改为index是因为多了一个虚拟节点,就不需要-1操作了
* for(int i=0;i<index;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);
}
}
有了上面add操作的遍历思路,我们可以写出根据索引查询某个元素,修改指定索引的元素,查询是否包含某个元素,以及链表的toString方法
public class LinkedList<E> {
// 定义成私有因为,使用者是无需关心Node的实现,只需使用
private class Node {
// 保存的数据
private E e;
// 指向下一个Node节点
private 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 dummyHead;
// 链表中元素的数量
private int size;
public LinkedList() {
dummyHead = new Node();
size = 0;
}
// 获取链表中元素数量
public int getSize() {
return size;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 在链表头部添加元素
public void addFirst(E e) {
add(0, e);
}
// 在指定位置添加元素
public void add(int index, E e) {
// 参数校验
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed, Illegal index.");
Node prev = dummyHead;
// 遍历出index前一个Node节点
for (int i = 0; i < index; 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);
}
// 根据索引查询某个元素
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);
}
//修改指定索引处的元素
public void set(int index, E e) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Set failed, Illegal index.");
Node cur = dummyHead;
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
cur.e = e;
}
//查询是否存在指定元素
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("LinkedList Size:%dn", size));
Node cur = dummyHead.next;
while(cur!=null) {
res.append(cur.e + " -> ");
cur = cur.next;
}
res.append("Null");
return res.toString();
}
}
只剩下最后一个删除操作了,想要删除链表中指定的元素,首先要通过遍历找到要删除元素(delNode)的前一个元素(prev),将prev的next指向delNode的next的节点,然后将delNode的next指向null,让GC回收这个空间
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);
}
写个测试用例跑下代码
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<Integer>();
for (int i = 0; i <= 5; i++) {
list.add(i, i + 1);
}
list.addFirst(6);
System.out.println(list);
System.out.println("是否存在2: " + list.contains(2));
System.out.println("是否存在7: " + list.contains(7));
list.set(4, 10);
System.out.println(list);
System.out.println("获取索引为4的元素: " + list.get(4));
System.out.println(list);
System.out.println("===================================");
System.out.println("删除最后一个元素: " + list.removeLast());
System.out.println(list);
System.out.println("===================================");
System.out.println("删除第一个元素: " + list.removeFirst());
System.out.println(list);
}
}
输出结果
LinkedList Size:7
6 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> Null
是否存在2: true
是否存在7: false
LinkedList Size:7
6 -> 1 -> 2 -> 3 -> 10 -> 5 -> 6 -> Null
获取索引为4的元素: 10
LinkedList Size:7
6 -> 1 -> 2 -> 3 -> 10 -> 5 -> 6 -> Null
===================================
删除最后一个元素: 6
LinkedList Size:6
6 -> 1 -> 2 -> 3 -> 10 -> 5 -> Null
===================================
删除第一个元素: 6
LinkedList Size:5
1 -> 2 -> 3 -> 10 -> 5 -> Null