链表(Linked List)
(在上篇博客的基础上继续)
四、链表的遍历,查询和修改
示例代码: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();
}
}
private Node dummyHead;
private int size;
public LinkedList(){
dummyHead = new Node();
size = 0;
}
// 获取链表中的元素个数
public int getSize(){
return size;
}
// 返回链表是否为空
public boolean isEmpty(){
return size == 0;
}
// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作,练习用:)
public void add(int index, E e){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
Node prev = dummyHead;
for(int i = 0 ; i < index ; i ++)
prev = prev.next;
prev.next = new Node(e, prev.next);
size ++;
}
// 在链表头添加新的元素e
public void addFirst(E e){
add(0, e);
}
// 在链表末尾添加新的元素e
public void addLast(E e){
add(size, e);
}
// 获得链表的第index(0-based)个位置的元素
// 在链表中不是一个常用的操作,练习用:)
public E get(int index){ //(新增代码)
if(index < 0 || index >= size) //判断 Index 合法性
throw new IllegalArgumentException("Get failed. Illegal index.");
Node cur = dummyHead.next; //Node 型 cur[当前] 从 dummyHead 的 next 从索引为0(也就是从第一个元素)开始
for(int i = 0 ; i < index ; i ++) //遍历元素
cur = cur.next;
return cur.e; //e 就是第 Index 位置的元素
}
// 获得链表的第一个元素
public E getFirst(){
return get(0);
}
// 获得链表的最后一个元素
public E getLast(){
return get(size - 1);
}
// 修改链表的第index(0-based)个位置的元素为e
// 在链表中不是一个常用的操作,练习用:)
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){ //判断当前节点是否为有效节点
if(cur.e.equals(e)) //判断 e 是否为用户传来的节点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 + "->"); //res 中加入 cur节点+ "->"
// cur = cur.next; //查看下一个节点
// }
for(Node cur = dummyHead.next ; cur != null ; cur = cur.next) //上面三行的优雅写法
res.append(cur + "->");
res.append("NULL"); //到达链表结尾
return res.toString();
}
}
Main.java
public class Main {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>(); //声明LinkedList,承载整型
for(int i = 0 ; i < 5 ; i ++){ //为 LinkedList 添加 从0-4 这5个元素
linkedList.addFirst(i);
System.out.println(linkedList);
}
linkedList.add(2, 666); //在索引为2的位置添加 666
System.out.println(linkedList);
}
}
输出:先将 0-4 这5个数添加到链表中;再将 666 添加到 索引为2【第三个】的位置。
五、从链表中删除元素
有虚拟头结点的链表如图所示:
想要删除索引为 2 的元素,步骤:
1.先找到所要删除元素的前一个节点,即为图中的 1
2.prev 对应节点的 next 就是所要删除的节点,称为 delNode,
3.将 prev 对应节点的 next 赋值称为要删除的节点 delNode 的 next;即 执行 prev.next = delNode.next,从某种意义上来说,就将索引为 2 的元素从链表中删除了。
4.让 索引为 2 的元素的 next 与链表整个脱离开来,即让 delNode 的 next 指向 NULL,即执行 delNode.next = null ;通过这个测试,就将索引为 2 的元素真正的从链表中删除。
示例代码: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();
}
}
private Node dummyHead;
private int size;
public LinkedList(){
dummyHead = new Node();
size = 0;
}
// 获取链表中的元素个数
public int getSize(){
return size;
}
// 返回链表是否为空
public boolean isEmpty(){
return size == 0;
}
// 在链表的index(0-based)位置添加新的元素e
// 在链表中不是一个常用的操作,练习用:)
public void add(int index, E e){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
Node prev = dummyHead;
for(int i = 0 ; i < index ; i ++)
prev = prev.next;
prev.next = new Node(e, prev.next);
size ++;
}
// 在链表头添加新的元素e
public void addFirst(E e){
add(0, e);
}
// 在链表末尾添加新的元素e
public void addLast(E e){
add(size, e);
}
// 获得链表的第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(0-based)个位置的元素为e
// 在链表中不是一个常用的操作,练习用:)
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){
if(cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}
// 从链表中删除index(0-based)位置的元素, 返回删除的元素
// 在链表中不是一个常用的操作,练习用:)
public E remove(int index){ //(添加代码)
if(index < 0 || index >= size) //验证 index 合法性
throw new IllegalArgumentException("Remove failed. Index is illegal.");
Node prev = dummyHead; //prev初始是从 dummyHead 开始
for(int i = 0 ; i < index ; i ++)
prev = prev.next; //prev存储待删除节点的之前的节点
Node retNode = prev.next; //prev对应节点的 next 就是所要删除的节点retNode
prev.next = retNode.next; //将 prev 对应节点的 next 赋值称为要删除的节点 retNode 的 next
retNode.next = null; //让 retNode 的 next 指向 NULL,将其从链表中删除
size --;
return retNode.e;
}
// 从链表中删除第一个元素, 返回删除的元素
public E removeFirst(){ //(添加代码)
return remove(0);
}
// 从链表中删除最后一个元素, 返回删除的元素
public E removeLast(){ //(添加代码)
return remove(size - 1);
}
// 从链表中删除元素e
public void removeElement(E e){ //(添加代码)
Node prev = dummyHead;
while(prev.next != null){
if(prev.next.e.equals(e))
break;
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size --;
}
}
@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);
linkedList.remove(2);
System.out.println(linkedList);
linkedList.removeFirst();
System.out.println(linkedList);
linkedList.removeLast();
System.out.println(linkedList);
}
}
输出:先将 0-4 这5个数添加到链表中;再将 666 添加到 索引为2【第三个】的位置;再将索引为 2 位置的666删除,再删除链表首元素,再删除链表末元素,得到最终结果。
六、链表时间复杂度分析
1.添加操作:O(n)
addLast(e) ---O(n) [需要从链表头开始比遍历到链表尾]
addFirst(e) ---O(1)[直接添加即可]
addIndex(e) ---O(n/2) = O(n)【均摊】
2.删除操作
removeLast(e) ---O(n)[需要从头找到最后一个元素的前一个位置的节点]
removeFirst(e) ---O(1)[虚拟头节点就是第一个元素的前一个节点]
removeIndex(e) ---O(n/2) = O(n)
3.修改操作(链表不支持随机访问)
set(index e) ---O(n)[修改元素必须从头遍历找到要修改元素的位置]
4.查找操作
get(index) ---O(n)[需要从头遍历整个链表]
contains(e) ---O(n)[需要从头遍历整个链表]
总结:
七、 使用链表实现栈
将链表头当做栈顶,用链表作为栈的底层实现来完成栈的结构
示例代码:
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> { //实现 Stack 接口
private LinkedList<E> list; //私有链表对象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){ //向栈中添加元素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<>(); //声明LinkedListStack的对象stack
for(int i = 0 ; i < 5 ; i ++){ //向栈中压入5个元素
stack.push(i);
System.out.println(stack);
}
stack.pop(); //从栈中取出元素
System.out.println(stack);
}
}
输出:
比较栈与链表的性能差异:
ArrayStack.java
public class ArrayStack<E> implements Stack<E> {
private Array<E> array;
public ArrayStack(int capacity){
array = new Array<>(capacity);
}
public ArrayStack(){
array = new Array<>();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public void push(E e){
array.addLast(e);
}
@Override
public E pop(){
return array.removeLast();
}
@Override
public E peek(){
return array.getLast();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack: ");
res.append('[');
for(int i = 0 ; i < array.getSize() ; i ++){
res.append(array.get(i));
if(i != array.getSize() - 1)
res.append(", ");
}
res.append("] top");
return res.toString();
}
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>();
for(int i = 0 ; i < 5 ; i ++){
stack.push(i);
System.out.println(stack);
}
stack.pop();
System.out.println(stack);
}
}
Main.java
import java.util.Random;
public class Main {
// 测试使用stack运行opCount个push和pop操作所需要的时间,单位:秒
private static double testStack(Stack<Integer> stack, int opCount){
long startTime = System.nanoTime(); //计时开始
Random random = new Random(); //生成随机数
for(int i = 0 ; i < opCount ; i ++)
stack.push(random.nextInt(Integer.MAX_VALUE)); //压入栈中
for(int i = 0 ; i < opCount ; i ++)
stack.pop(); //从栈中拿出
long endTime = System.nanoTime(); //计时结束
return (endTime - startTime) / 1000000000.0; //纳秒转换为秒
}
public static void main(String[] args) {
int opCount = 100000;
ArrayStack<Integer> arrayStack = new ArrayStack<>();
double time1 = testStack(arrayStack, opCount);
System.out.println("ArrayStack, time: " + time1 + " s");
LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
double time2 = testStack(linkedListStack, opCount);
System.out.println("LinkedListStack, time: " + time2 + " s");
// 其实这个时间比较很复杂,因为LinkedListStack中包含更多的new操作
}
}
输出:
链表栈要比数组栈要快一些,数组栈中,经常需要重新分配整个静态数组,将原来的静态元素分配到新的数组中,比较耗时。
但这个结果不一定对,因为这二者的时间复杂度都是同一级别的,不存在过多的差距。如果输入量增到1000000时,可能数组栈更快了,链表每次new一个空间需要耗时。
八、 使用链表实现队列
在链表的头进行增加或删除操作比较容易因为有 head 帮助我们标记头部,想要在尾部也进行该操作,加入 tail 帮助我们标记尾部即可使增加操作更加容易,但删除操作需要找到 tail 的前一项,也只能通过遍历的方式来找到。故从 head 端删除元素,从tail端插入元素;如果链表为空时,由于没有 dummyHead,要注意链表为空的情况。
示例代码:
Queue.java
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
LinkedListQueue.java
public class LinkedListQueue<E> implements Queue<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();
}
}
private Node head, tail; //定义 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); //出入元素e
head = tail;
}
else{
tail.next = new Node(e); //new 的节点应该在 tail 的 next 的位置
tail = tail.next;
}
size ++;
}
@Override
public E dequeue(){ //出队操作
if(isEmpty()) //队列为空则抛出异常
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
Node retNode = head; //出队元素所在的节点应该是 Head 这个位置
head = head.next; //新的 head 将会跳过 Node ,直接指向 head.next
retNode.next = null; //将 retNode 从链表中断开
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 ++){ //入队10个元素
queue.enqueue(i);
System.out.println(queue);
if(i % 3 == 2){ //每隔3个元素出队1个元素
queue.dequeue();
System.out.println(queue);
}
}
}
}
输出:
比较数组、队列、与链表的性能差异:
示例代码:
Main.java
import java.util.Random;
public class Main {
// 测试使用q运行opCount个enqueueu和dequeue操作所需要的时间,单位:秒
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 + " s");
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue, time: " + time2 + " s");
LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();
double time3 = testQueue(linkedListQueue, opCount);
System.out.println("LinkedListQueue, time: " + time3 + " s");
}
}
输出:数组队列--O(n);循环队列--O(1);链表实现队列--O(1)