链表LinkedList
文章目录
链表是一种动态数据结构,不需要根据添加数据而调整存储空间。
数据存储在节点(Node)中
// Java中关于节点的定义
class Node<E>{
E data;
Node next;
}
优点:真正的动态,不需要处理固定容量的问题
缺点:没有随机访问的能力(链表在内存中存储是不是连续,需要根据上一个节点,才能找到下一个节点)
与数组Array对比:数组最好用于索引有语境的情况,支持随机快速查询。链表适合顺序查询,不适合用于索引有语境的情况
链表的实现
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 head;
int size;
//链表初始化时,只有头结点且为空,长度size=0
public LinkedList(){
head = null;
size =0;
}
/*一些常用方法*/
//获取链表长度
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size==0;
}
}
(1)添加元素
- 图解:
- 代码:
//在链表头添加元素
public void addHead(E e) {
Node node = new Node(e);
node.next = head;
head = node;
//添加元素后,size也会发生变化
size++;
}
(2)向指定位置插入元素
关键:找到添加位置的前一个节点
-
图解:
-
代码:
public void add(int index, E e) {
//判断插入的位置是否合适
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add Failed. Illegal index.");
}
// index=0即往头部添加元素
if (index == 0) {
addHead(e);
} else {
//从头结点遍历,找到要插入位置的前一个节点prev
Node prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
//让插入节点node指向prev的下一个节点
//prev.next = new Node(e,prev.next);
Node node = new Node(e);
node.next = prev.next;
prev.next = node;
//添加元素后,size也会发生变化
size++;
}
(3)设立dummyHead
方便在向指定位置添加元素时,避免对向index=0的位置添加元素时进行特殊处理
- 图解:
- 代码:
//加入虚拟节点,对以上代码重新实现
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() {
this(null, null);
}
public Node(E e) {
this(e, null);
}
@Override
public String toString() {
return e.toString();
}
}
// 链表属性
public int size;
public Node dummyHead; // 虚拟头结点
// 链表构造方法,初始化链表时,size=0;
public LinkedList() {
size = 0;
dummyHead = new Node();
}
@Override
public String toString() {
Node curNode = dummyHead.next;
StringBuilder res = new StringBuilder();
res.append("Head:");
for (int i = 0; i < size; i++) {
res.append(curNode.e + "->");
curNode = curNode.next;
}
res.append("null");
return res.toString();
}
// 链表长度
public int getSize() {
return size;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 向链表中指定位置index(0~size-1)添加元素
public void add(int index, E e) {
// 判断index是否合法
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed, illegal index.");
}
// 使用prev指向目标位置的前一个元素
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// 插入元素
Node target = new Node(e, prev.next);
prev.next = target;
// 注意添加元素之后,size也会发生变化
size++;
}
// 获取指定位置的元素
public E get(int index) {
// 判断index是否合法
if (index < 0 || index > size) {
throw new IllegalArgumentException("Get failed, illegal index.");
}
// 使用cur指向目标元素
Node curNode = dummyHead.next;
for (int i = 0; i < index; i++) {
curNode = curNode.next;
}
return curNode.e;
}
// 修改指定位置的元素
public void set(int index, E e) {
// 判断index是否合法
if (index < 0 || index > size) {
throw new IllegalArgumentException("Set failed, illegal index.");
}
// 使用cur指向目标元素
Node curNode = dummyHead.next;
for (int i = 0; i < index; i++) {
curNode = curNode.next;
}
curNode.e = e;
}
// 判断是否包含指定元素
public boolean contains(E e) {
Node curNode = dummyHead.next;
for (int i = 0; i < size; i++) {
// null值之间的比较使用==,使用equals发生空指针异常
// 注意:使用null==.. 而不是 ..==null
if (e == curNode.e) {
return true;
}
//引用类型比较,使用equals
if (e.equals(curNode.e)) {
return true;
} else {
curNode = curNode.next;
}
}
return false;
}
// 删除指定位置元素,并返回该元素
public E delete(int index) {
// 判断index是否合法
if (index < 0 || index > size) {
throw new IllegalArgumentException("Set 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 static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
// 添加元素测试
for (int i = 0; i < 5; i++) {
list.add(list.size,i);//默认添加到链表尾部
System.out.println(list);
}
// 指定位置添加元素
list.add(2, 222);
System.out.println(list);
// 修改元素测试
list.set(2, 2);
System.out.println(list);
// 删除元素测试
list.delete(2);
System.out.println(list);
list.add(0,null);
System.out.println(list);
System.out.println(list.contains(1));//true
System.out.println(list.contains(null));//true
System.out.println(list.contains(7));//false
}
}
(4)删除指定位置的元素
- 图解:
- 代码
// 删除指定位置元素,并返回该元素
public E delete(int index) {
// 判断index是否合法
if (index < 0 || index > size) {
throw new IllegalArgumentException("Set 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;
}
时间复杂度分析
-
添加操作
方法 时间复杂度 addLast(e) // add(size,e) O(n) addHead(e) // add(0,e) O(1) add(index ,e) O(n/2)=O(n) -
删除操作
方法 时间复杂度 deleteLast(e) // delete(size,e) O(n) deleteHead(e) // delete(0,e) O(1) delete(index ,e) O(n/2)=O(n) -
修改操作
方法 时间复杂度 set(index,e) // set(index,e) O(n) -
查找操作
方法 时间复杂度 get(index) O(n) contains(e) O(n)
总结:
- 增加操作: O(n)
- 删除操作: O(n)
- 更改操作: O(n)
- 查询操作: O(n)
- 需要注意的是,如果只对链表头进行操作,时间复杂度为:O(1)
链表的应用
(1)使用链表实现栈
根据操作链表的时间复杂度分析,在只对链表的头结点进行操作时,时间复杂化度为O(1).根据此特性,据此可以使用链表来实现栈。
- 代码:
1.栈Stack
Interface Stack<E>{
void push(E); //入栈
E pop(E e); //出栈
E peek(E e); //取出栈顶元素
int getSize(); //获取栈中的元素个数
boolean isEmpty(); //判断栈是否为空
}
2. 链表实现
package com.LinkedList;
import com.Stack.Stack;
/**
* @author Ming
* @create 2019-06-20 22:01
*/
public class LinkedListStack<E> implements Stack<E> {
private LinkedList1<E> linkedList;
public LinkedListStack() {
this.linkedList = new LinkedList1<E>();
}
@Override
public int getSize() {
return linkedList.getSize();
}
@Override
public boolean isEmpty() {
return linkedList.isEmpty();
}
//入栈操作
//对于底层的链表来说,相当于向头结点添加元素
@Override
public void push(E e) {
linkedList.addHead(e);
}
//出栈操作,并返回该元素
//对于底层的链表来说,相当于删除头结点并返回该元素
@Override
public E pop() {
return linkedList.removeHead();
}
//取出栈顶元素
@Override
public E peek() {
return linkedList.getHead();
}
@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);
}
}
}
(2)使用链表实现队列
链表改进:使用tail标记链表的尾节点,方便对链表尾部的元素进行操作,这样从两端插入元素都是容易的。据此可以使用链表来实现队列。
-
注意:
-
在队列中,一般只会对队首以及队尾元素进行操作,不用对队中的元素进行操作,因此在底层实现的链表中,不再使用虚拟头结点dummyHead
-
由于不存在dummyHead,需要注意链表为空的情况,此时队首head与队尾tail会处于统一位置
-
-
图解:
- 代码:
package com.LinkedList;
/**
* @author Ming
* @create 2019-06-21 10:06
*/
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; //头结点
private Node 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;
tail.next = null;
}
size++;
}
//出队(移除链表头部元素)
@Override
public E deQueue() {
if (isEmpty()) {
throw new IllegalArgumentException("DeQueue failed, Empty queue");
}
Node resultNode = head;
head = head.next;//头结点出队,让当前head的下一个节点,作为新的节点
resultNode.next = null;//方便回收
//若出队后,队列为空
if (head == null) {
//头结点出队后,tail节点仍然指向之前的尾节点
tail = null;
}
size--;
return resultNode.e;
}
//获取头结点元素
@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalArgumentException("Get failed, Empty queue");
}
return head.e;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Queue:front ");
Node cur = head;
while (cur != null) {
res.append(cur.e + "->");
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 < 6; i++) {
queue.enQueue(i);
System.out.println(queue);
if(i%3==2){
queue.deQueue();
}
}
}
}
补充:尝试使用含有dummyHead的链表实现队列
链表与递归
举例:leetCode 203 移除链表元素
Category | Difficulty | Likes | Dislikes |
---|---|---|---|
algorithms | Easy (41.09%) | 247 | - |
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
解答:
(1)链表中未包含dummyHead
/*
* @lc app=leetcode.cn id=203 lang=java
*
* [203] 移除链表元素
*/
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
//链表中的删除元素,理解为:让目标节点delNode的前一个节点pre.next指向delNode.next即可
class Solution {
public ListNode removeElements(ListNode head, int val) {
//先处理头结点元素
//包括删除当前头结点而产生的新的头结点
while (head != null && head.val == val) {
head = head.next;
}
//处理完成之后,head=null(即链表为空)
if (head == null) {
return null;
}
//处理中间的元素
//需要先使用prev指向目标元素的前一个位置
ListNode prev = head;
while (prev.next != null) {
//指定位置的值满足条件,则删除
if (prev.next.val == val) {
prev.next = prev.next.next;
} else {
prev = prev.next;
}
}
return head;
}
}
(2)链表中包含dummyHead(避免了对head是否为空的特殊处理)
/*
* @lc app=leetcode.cn id=203 lang=java
*
* [203] 移除链表元素
*/
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
//定义虚拟头节点,其next指向真正的头结点
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
//删除指定位置节点,需要先使用prev指向目标元素的前一个位置
ListNode prev = dummyHead;
while(prev.next!=null){
if(prev.next.val==val){
prev.next = prev.next.next;
}else{
prev = prev.next;
}
}
return dummyHead.next;
}
}
(3)递归
链表中的递归:
使用递归解决上述问题:(LeetCode203:删除链表中的相同元素)
-
图解
-
代码:
/*
* @lc app=leetcode.cn id=203 lang=java
*
* [203] 移除链表元素
*/
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
ListNode res = removeElements(head.next, val);
if (head.val == val) {
return res;
} else {
head.next = res;
return head;
}
}
}