链表
1.定义
-
链表是一种线性的动态数据结构,与动态数组,栈,队列的“动态”不同(这三种数据结构的底层都是静态数组,依托resize解决容量问题),链表是真正意义上的动态数据结构
-
链表与数组相比,失去了随机访问的能力,但却拥有动态的优点(不需要处理固定容量问题)
-
链表的数据存储在Node中,Node由两部分组成,一部分存储数据,一部分存储指向的下一个Node的引用(或指针)
-
链表可以辅助组成其他数据结构(图、哈希表、队列、栈等等)
2.链表的基本实现
// 不使用虚拟头节点实现链表的增删改查
public class LinkedList<E> {
private class Node {
E e;
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 newNode = new Node(e);
newNode.next = head;
head = newNode;
size++;
// head = new Node(e,head); // 优化
}
// 向链表index位置添加元素
public void add(E e, int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index");
}
// 在头部添加的情况单独考虑
if (index == 0) {
addFirst(e);
} else {
Node pre = head;
// 找到待插入位置节点的前驱节点
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
Node newNode = new Node(e);
newNode.next = pre.next;
pre.next = newNode;
// 同样该操作可以优化为
// pre.next = new Node(e, pre.next);
size++;
}
}
// 在链表尾部添加新元素
public void addLast(E e) {
add(e,size);
}
// 获取链表index位置的元素
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed . Illegal index");
}
Node cur = head;
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(E e,int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Illegal index");
}
Node cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.e = e;
}
// 判断是否包含元素e
public boolean contains(E e) {
Node cur = head;
while (cur != null) {
if (cur.e.equals(e)) {
return true;
}
cur = cur.next;
}
return false;
}
// 从链表中删除第一个元素
public E removeFirst() {
if (isEmpty()) {
throw new IllegalArgumentException("Remove failed. the list is empty");
}
Node ret = head.next;
head = ret;
size--;
return ret.e;
}
// 从链表中删除index位置中的元素
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Illegal index");
}
// 删除头节点的情况特殊处理
if (index == 0) {
return removeFirst();
} else {
// 找到index位置元素的前驱节点
Node pre = head;
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
// 待删除节点
Node ret = pre.next;
pre.next = ret.next;
ret.next = null;
size--;
return ret.e;
}
}
// 从链表中删除最后一个元素
public E removeLast() {
return remove(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = head;
while (cur != null) {
res.append(cur.e + " -> ");
cur = cur.next;
}
res.append("null");
return res.toString();
}
}
3.使用虚拟头节点
- 在上述链表的基本实现中,我们把链表中的第一个元素定义为头节点,这种情况下在头节点处添加新元素和删除头节点的情况需要特殊处理(因为增删操作都需要找到目标元素的前驱节点,而头节点并没有前驱节点)
- 接下来我们使用一个虚拟头节点(作为链表第一个元素的前驱节点),来对上诉代码进行优化,使得在任何情况下增删操作都一致
public class LinkedList<E> {
private class Node {
E e;
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(null,null);
size = 0;
}
// 获取链表的元素个数
public int getSize() {
return size;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 向链表头部添加元素
public void addFirst(E e) {
add(e,0);
}
// 向链表index位置添加元素
public void add(E e, int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Illegal index");
}
// 设置虚拟头节点后的实现 不需要考虑插入头部的情况
Node pre = dummyHead;
// 找到插入位置前驱节点
for (int i = 0; i < index; i++) {
pre = pre.next;
}
pre.next = new Node(e, pre.next);
size++;
}
// 在链表尾部添加新元素
public void addLast(E e) {
add(e,size);
}
// 获取链表index位置的元素
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(E e,int index) {
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位置中的元素
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Illegal index");
}
Node pre = dummyHead;
// 找到index位置元素的前驱节点
for (int i = 0; i < index; i++) {
pre = pre.next;
}
// 待删除节点
Node ret = pre.next;
pre.next = ret.next;
ret.next = null;
size--;
return ret.e;
}
// 从链表中删除第一个元素
public E removeFirst() {
return remove(0);
}
// 从链表中删除最后一个元素
public E removeLast() {
return remove(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur.e + " -> ");
cur = cur.next;
}
res.append("null");
return res.toString();
}
}
4.用链表实现栈
- 栈还可以通过链表来实现 => 链式栈
// 首先编写栈的接口
public interface Stack<E> {
public int getSize();
public boolean isEmpty();
public void push(E e);
public E pop();
public E peek();
}
// 实现类
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> linkedList;
public LinkedListStack() {
linkedList = new LinkedList<>();
}
@Override
public int getSize() {
return linkedList.getSize();
}
@Override
public boolean isEmpty() {
return linkedList.isEmpty();
}
@Override
public void push(E e) {
// 在链表头添加新元素
linkedList.addFirst(e);
}
@Override
public E pop() {
// 从链表头删除元素
return linkedList.removeFirst();
}
@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);
}
}
5.用链表实现队列(尾指针)
- 队列的操作涉及到在队首删除元素(删除头节点),在队尾添加元素(在链表尾部添加元素),前者的复杂度可以达到O(1)的级别,但后者的复杂度却达到了O(n)的级别(从头节点遍历到尾部),有没有办法可以让其变成O(1)的级别呢,自然是可以的。我们可以在链表中设立一个尾指针(跟踪链表的最后一个元素),从而实现O(1)的复杂度
/*
* 改进链表 => 使用带尾指针的链表实现队列
* */
public class LinkedListQueue<E> implements Queue<E> {
private class Node {
E e;
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);
}
}
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) { // 此时head == 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 ret = head;
head = head.next;
ret.next = null;
if (head == null) {
// 注意,当head==null,说明链表中只有一个元素,要记得维护tail
tail = null;
}
size--;
return ret.e;
}
@Override
public E peek() {
if (isEmpty()) {
throw new IllegalArgumentException("Cannot peek from an 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 < 5; i++) {
queue.enqueue(i);
System.out.println(queue);
}
System.out.println("===============================");
for (int i = 3; i > 0; i--) {
queue.dequeue();
System.out.println(queue);
}
}
}
6.LeetCode203-移除链表元素
此题考察的是链表的删除操作,前面我们可以知道,在设立虚拟头节点的情况下,删除任何节点都一样。但是在没有设立虚拟头节点的情况下,当删除链表的头节点时,需要特殊处理
解法一 不使用虚拟头节点
public class Solution {
// 解法一:不使用虚拟头节点
public ListNode removeElements(ListNode head, int val) {
// 删除头节点的情况
while (head != null && head.val == val) {
head = head.next;
}
if (head == null) {
return head;
}
ListNode pre = head;
while (pre.next != null) {
if (pre.next.val == val) {
pre.next = pre.next.next;
} else {
pre = pre.next;
}
}
return head;
}
}
解法二 使用虚拟头节点
public class Solution {
// 解法二: 使用虚拟头节点
public ListNode removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode(-1);// 虚拟头节点
dummyHead.next = head;
ListNode pre = dummyHead;
while (pre.next != null) {
if (pre.next.val == val) {
pre.next = pre.next.next;
} else {
pre = pre.next;
}
}
return dummyHead.next;
}
}
7.链表的天然递归结构
- 链表具有一个天然的递归结构,即一个链表可以看成一个头节点和一个较短的链表
当涉及到链表的问题时,通常有递归的解法,例如Leetcode的203问题,就可以通过递归来实现
public class Solution {
// 解法三:递归实现
public ListNode removeElements(ListNode head, int val) {
// 基线条件
if (head == null) {
return head;
}
ListNode res = removeElements(head.next, val); // 子过程
if (head.val == val) {
return res;
} else {
head.next = res;
return head;
}
}
}
8.递归
-
递归的本质,就是将原问题转化为更小的同一类问题,最后将子问题的解组合为原问题的解
-
如何考虑递归问题:
- 递归的宏观语义
// 宏观语义 => 在以head为头节点的链表中删除值为val的节点
public ListNode removeElements(ListNode head, int val)
// 子过程的解
ListNode res = removeElements(head.next, val)
// 将子过程的解还原为原过程
if (head.val == val) {
return res;
} else {
head.next = res;
return head;
}
9.链表的递归实现
- 使用递归实现链表的增删改查操作
/*
* 使用递归实现链表的增删改查
* */
public class LinkedList<E> {
private class Node {
E e;
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);
}
}
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位置添加元素
public void add(E e, int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add is faild : The index is out of bounds");
}
Node pre = dummyHead;
add(pre, e, index);
size++;
}
// 在以head为虚拟头节点的index位置插入新元素
private void add(Node head, E e, int index) {
// 基线条件
if (index == 0) {
head.next = new Node(e,head.next);
return;
}
add(head.next, e, index - 1);
}
public void addFirst(E e) {
add(e, 0);
}
public void addLast(E e) {
add(e, size);
}
// 从链表中删除index位置的元素
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove faild : the index is out of bound");
} else if (isEmpty()) {
throw new IllegalArgumentException("Remove faild : the LinkedList is empty");
}
Node pre = dummyHead;
size--;
return remove(pre, index);
}
// 从以head为虚拟头结点的链表中,删除第index位置的元素,递归算法
private E remove(Node head, int index) {
if (index == 0) {
Node res = head.next;
head.next = res.next;
return res.e;
}
return remove(head.next,index - 1);
}
public E removeFirst() {
return remove(0);
}
public E removeLast() {
return remove(size - 1);
}
public void set(E e, int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set faild : the index is out of bound");
} else if (isEmpty()) {
throw new IllegalArgumentException("Set faild : the LinkedList is empty");
}
Node cur = dummyHead.next;
set(cur, e, index);
}
// 修改以head为头节点的链表index处的元素
private void set(Node head, E e, int index) {
if (index == 0) {
head.e = e;
return;
}
set(head.next, e, index - 1);
}
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get faild : the index is out of bound");
} else if (isEmpty()) {
throw new IllegalArgumentException("Get faild : the LinkedList is empty");
}
Node cur = dummyHead.next;
return get(cur, index);
}
// 获取以head为头节点的链表index位置处的元素
private E get(Node head, int index) {
if (index == 0) {
return head.e;
}
return get(head.next, index - 1);
}
public E findFirst() {
return get(0);
}
public E findLast() {
return get(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur.e + " -> ");
cur = cur.next;
}
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);
}
System.out.println("设置index = 3的元素为66");
linkedList.set(66,3);
System.out.println(linkedList);
System.out.println("在index = 2处添加元素99");
linkedList.add(99,2);
System.out.println(linkedList);
System.out.println("index = 2位置处的元素为: " + linkedList.get(2));
for (int i = 0; i < 3; i++) {
linkedList.removeFirst();
System.out.println(linkedList);
}
System.out.println("删除index = 1位置的元素");
linkedList.remove(1);
System.out.println(linkedList);
}
}
10.LeetCode206-反转链表
解法一 迭代:
- 使用两个临时变量pre和next,保存链表的前驱和后继节点,从而实现链表的反转
public class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head;;
ListNode pre = null;
while (cur != null) {
ListNode nextTemp = cur.next;
cur.next = pre;
pre = cur;
cur = nextTemp;
}
return pre;
}
}
解法二 递归:
public class Solution {
public ListNode reverseList(ListNode head) {
// 基线条件
if (head == null || head.next == null) {
return head;
}
ListNode rev = reverseList(head.next);// 返回反转后的子链表头节点
head.next = head.next.next;
head.next = null;
return rev;
}
}