搞懂数据结构-链表
1.背景
动态数组的申请数组大小有个明显的缺点,可能会造成内存空间的大量浪费。那能不能需要用多少就申请多少内存大小呢?链表可以做到这一点。
2.单向链表
2.1单向链表的设计
代码:
public class LinkedList<E> {
//存储元素的大小
private int size;
//指向第一个元素的节点
private Node<E> first;
//因为节点类只在本类中使用,所以定义为内部类
private static class Node<E>{
E element;
Node<E> next;
//构造方法
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
}
2.1.1设计思路
链表的接口设计和动态数组大同小异,所以在这里抽取一个公共接口出来。如果不写接口,而是写父类,让继承的话,就会存在方法的差异。所以更好的方法是,定义一个接口,然后再定义个抽象类,抽象类中放公共方法,而子类中不同的方法重写接口中的就行。没有必要在父类中写不是公共方法的代码。
抽象类存在的好处,如果子类直接去实现接口,那么接口里面的方法全部都得实现,而抽象类不需要
理解:
- 先抽象出一个抽象类,里面存放公共方法,然后再向上再抽象出一个接口,存放所有方法,包括不公共的方法
- 或者先抽象出接口,然后子类都去实现接口,发现子类有公共代码,将公共代码抽取出来成抽象成一个类B。让这个类B去实现接口,而子类去实现这个类B。但是只要是实现了接口就必须重写接口中所有方法,而类B的功能只是存放公共方法,所有这时候将类B变成抽象类就可以解决这个问题
抽象类不对外公开,只提供公共代码的作用。所有在使用过程中一般
List<Integer> list = new LinkedList<>();
2.1.2接口设计
public interface List<E> {
public static final int ELEMENT_NOT_FIND = -1;
//元素大小
int size();
//判断是否为空
boolean isEmpty();
//添加元素到指定位置
void add(int index,E element);
//添加元素的末尾
void add(E element);
//删除指定元素
E remove(int index);
//清空元素
void clear();
//修改元素
E set(int index, E element);
//判断是否含有
boolean contain(E element);
//查找元素
E get(int index);
}
2.1.3抽象类
类似于动态数组设计中一样,我们将一些通用的方法写在一个抽象类中,那么新类继承抽象类就可以直接使用了,并且让抽象类实现接口,并实现一些通用方法
public abstract class AbstractList<E> implements List<E> {
protected int size;
// 下标越界抛出的异常
protected void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
// 检查下标越界(不可访问或删除size位置)
protected void rangeCheck(int index){
if(index < 0 || index >= size){
outOfBounds(index);
}
}
// 检查add()的下标越界(可以在size位置添加元素)
protected void rangeCheckForAdd(int index) {
if (index < 0 || index > size) {
outOfBounds(index);
}
}
@Override
public boolean contain(E element) {
return indexOf(element) != ELEMENT_NOT_FOUND;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(E element) {
add(size,element);
}
}
2.2单向链表的增删改查
public class SingledList<E> extends AbstractList<E> {
private Node<E> first;
private static class Node<E>{
E element;
Node<E> next;
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
}
查
//找到索引对应的值
@Override
public E get(int index) {
return node(index).element;
}
//找到索引对应的node
public Node<E> node(int index){
rangeCheck(index);
Node node = first;
for (int i = 0; i < index; i++){
node = node.next;
}
return node;
}
@Override
public int indexOf(E element) {
if (element == null){
Node<E> node = first;
for (int i = 0; i < size; i++){
if (node.element == null) return i;
node = node.next;
}
}else {
Node<E> node = first;
for (int i = 0; i < size; i++){
if (node.element.equals(element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
改
//修改
@Override
public E set(int index, E element) {
//查找节点,node做了索引边界检测,所以这里不需要再做
Node<E> node = node(index);
E old = node.element;
//修改element
node.element = element;
return old;
}
增
//添加元素
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == 0){
//first指向新Node,新Node原来的first指向的node,也就是first
first = new Node<>(element,first);
}else {
//需要找到前面的元素
Node<E> pre = node(index - 1);
//pre指向新Node,新Node指向pre.next
pre.next = new Node<>(element,pre.next);
}
size++;
}
删
//删除
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = first;
if (index == 0){
first = first.next;
}else {
//要删除节点的前一个
Node<E> pre = node(index - 1);
//要删除的节点
node = pre.next;
//前一个节点指向要删除的后一个节点
pre.next = node.next;
}
size--;
//返回被删除节点的元素值
return node.element;
}
清空元素
只需要将size设置为0,fisrt指向null,后面的 Node没有被指向,在 Java 中会自动被垃圾回收。
@Override
public void clear() {
size = 0;
first = null;
}
完整代码
public class SingledList<E> extends AbstractList<E> {
private Node<E> first;
private static class Node<E>{
E element;
Node<E> next;
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
//添加元素
@Override
public void add(int index, E element) {
if (index == 0){
//first指向新Node,新Node原来的first指向的node,也就是first
first = new Node<>(element,first);
}else {
//需要找到前面的元素
Node<E> pre = node(index - 1);
//pre指向新Node,新Node指向pre.next
pre = new Node<>(element,pre.next);
}
size++;
}
//删除
@Override
public E remove(int index) {
Node<E> node = first;
if (index == 0){
first = first.next;
}else {
//要删除节点的前一个
Node<E> pre = node(index - 1);
//要删除的节点
node = pre.next;
//前一个节点指向要删除的后一个节点
pre.next = node.next;
}
size--;
//返回被删除节点的元素值
return node.element;
}
@Override
public void clear() {
size = 0;
first = null;
}
//修改
@Override
public E set(int index, E element) {
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
//找到索引对应的值
@Override
public E get(int index) {
return node(index).element;
}
//找到索引对应的node
public Node<E> node(int index){
rangeCheck(index);
Node node = first;
for (int i = 0; i < index; i++){
node = node.next;
}
return node;
}
@Override
public int indexOf(E element) {
if (element == null){
Node<E> node = first;
for (int i = 0; i < size; i++){
if (node.element == null) return i;
node = node.next;
}
}else {
Node<E> node = first;
for (int i = 0; i < size; i++){
if (node.element.equals(element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
}
小结:
可以注意到,增和删在头节点这里的处理和其他位置的处理略有不同。因为头节点只是一个指针,并不是一个节点,没有next属性。
2.3 虚拟头节点
为了统一增和删的逻辑操作,增加一个虚拟头节点来实现。(解题的时候可以添加虚拟头节点这个来简化代码)
发生变化的地方
1.构造方法
//构造初始虚拟节点
public SingledList() {
this.first = new Node<E>(null, null);
}
2.查找节点方法
//找到索引对应的node
public Node<E> node(int index){
rangeCheck(index);
//以前是first,现在是first.next
//因为现在first是虚拟节点,里面有next属性
Node node = first.next;
for (int i = 0; i < index; i++){
node = node.next;
}
return node;
}
3.增方法
//添加元素
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
//需要找到前面的元素,如果index为0,index-1会出现负数,所以需要判断
Node<E> pre = index == 0 ? first:node(index - 1);
//pre指向新Node,新Node指向pre.next
pre = new Node<>(element,pre.next);
size++;
}
4.删方法
//删除
@Override
public E remove(int index) {
rangeCheck(index);
//要删除节点的前一个
Node<E> pre = index == 0 ? first:node(index - 1);
//要删除的节点
Node<E> node = pre.next;
//前一个节点指向要删除的后一个节点
pre.next = node.next;
size--;
//返回被删除节点的元素值
return node.element;
}
3.双向链表
3.1双向链表的设计
在单向链表中,要访问尾节点,每次都要从头节点开始。为了提升性能,双向链表可以进行优化。
双向链表的接口和抽象都和单向链表一样,只是在增删改查中不一样。
代码
public class DoubleLinkedList<E> extends AbstractList<E> {
//头尾节点
private Node<E> first;
private Node<E> last;
private static class Node<E>{
E element;
Node<E> prev;
Node<E> next;
public Node(E element, Node<E> prev, Node<E> next) {
this.element = element;
this.prev = prev;
this.next = next;
}
}
3.2双向链表的增删改查
查
//根据索引获取元素
@Override
public E get(int index) {
return node(index).element;
}
//根据索引找到节点
public Node<E> node(int index){
rangeCheck(index);
if (index < (size >> 1)){
Node<E> node = first;
for (int i = 0; i < index ; i++){
node = node.next;
}
return node;
}else {
Node<E> node = last;
for (int i = size -1; i > index ; i++){
node = node.next;
}
return node;
}
}
改
//修改元素
@Override
public E set(int index, E element) {
E old = node(index).element;
node(index).element = element;
return old;
}
增
/**
* 添加元素:
* 尾节点添加,特殊情况是添加第一个
* 中间添加,特殊情况是头节点
* @param index
* @param element
*/
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == size){
Node<E> oldLast = last;
last = new Node<>(element,oldLast,null);
if (oldLast == null){
//第一个节点,头尾指针指向同一个
first = last;
}else {
oldLast.next = last;
}
}else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(element,prev,next);
next.prev = node;
//index = 0,头节点添加不一样
if (prev == null){
first = node;
}else {
prev.next = node;
}
}
size++;
}
删
//清空链表
@Override
public void clear() {
size = 0;
first = null;
last = null;
}
//删除元素
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(index);
Node<E> prev = node.prev;
Node<E> next = node.next;
//头部
if (prev == null){
first = next;
}else {
prev.next = next;
}
//尾部
if (next == null){
last = prev;
}else {
next.prev = prev;
}
size--;
return node.element;
}
4.单向环形链表
和单向链表对比,操作不同的地方在于增和删除的头部处理
增
//添加元素
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == 0){
Node<E> newFirst = new Node<>(element, first);
// 拿到最后一个节点, 上面先不要直接改first, 否则下面找节点会出现问题
Node<E> last = (size == 0) ? newFirst : node(size - 1);
last.next = newFirst;
first = newFirst;
}else {
//需要找到前面的元素
Node<E> pre = node(index - 1);
//pre指向新Node,新Node指向pre.next
pre.next = new Node<>(element,pre.next);
}
size++;
}
删
//删除
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = first;
if (index == 0){
if (size == 1){
first = null;
}else{
Node<E> last = node(size - 1);
first = first.next;
last.next = first;
}
}else {
//要删除节点的前一个
Node<E> pre = node(index - 1);
//要删除的节点
node = pre.next;
//前一个节点指向要删除的后一个节点
pre.next = node.next;
}
size--;
//返回被删除节点的元素值
return node.element;
}
5.双向循环链表
和双向向链表对比,操作不同的地方在于增和删除的头部操作
增
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
if (index == size){
Node<E> oldLast = last;
last = new Node<>(element,oldLast,first);
if (oldLast == null){
//第一个节点,头尾指针指向同一个
first = last;
first.next = first;
first.prev = first;
}else {
oldLast.next = last;
first.prev = last;
}
}else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(element,prev,next);
next.prev = node;
prev.next = node;
//index = 0,头节点添加不一样
if (next == first){
first = node;
}
}
size++;
}
删
//删除元素
@Override
public E remove(Node<E> node) {
if (size == 1) {
first = null;
last = null;
} else {
Node<E> pre = node.pre;
Node<E> next = node.next;
pre.next = next;
next.pre = pre;
if (node == first) { // index == 0
first = next;
}
if (node == last) { // index == size - 1
last = pre;
}
}
size--;
return node.element;
}
6.静态链表
- 可以通过数组来模拟链表,称为静态链表
- 数组的每个元素存放 2 个数据:值、下个元素的索引
- 数组 0 位置存放的是头结点信息
如果一个元素只能存放1个数据
- 那就使用 2 个数组,1 个数组存放索引关系,1 个数组存放值
练习
1.删除链表中的节点
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
分析:我们之前的思路是找到要删除节点的前一个节点,但是太麻烦,可以采取用节点覆盖的方法
注意:这里没有尾节点,有尾节点这个方法就不能用了
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
//用被删除的节点的下一个节点进行覆盖
node.val = node.next.val;
node.node = node.next.next;
}
}
2.删除链表中等于给定值 val 的所有节点。
分析:注意删除的头结点是不一样的。所以加了一个虚头节点
代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
//新加一个虚头节点,让头节点和其他节点的操作统一起来
ListNode header = new ListNode(-1);
header.next = head;
ListNode cur = header;
while (cur.next != null){
if (cur.next.val == val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return header.next;
}
}
3.删除排序链表中重复元素
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
分析:根据有序,相邻判断是否相等就行
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode header = new ListNode(10000);
header.next = head;
ListNode cur = header;
while (cur.next != null){
if (cur.val == cur.next.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return header.next;
}
}
4.反转一个链表
反转一个单链表。
例子:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NUL
递归方法
假设方法实现了,那么调用方法会变成怎么样呢(这个思考很关键,利用递归,必须得先知道递归的函数的作用)
调用下一个,这里执行成功后,开始归的代码
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
//边界条件
if (head==null || head.next==null) return head;
// 这个是递
ListNode newHead = reverseList(head.next);
// 拿到4,也就是head.next
// 让4,指向下一个节点就是head,这里是归
//下面的归操作,就是newHead-1-2-3-4,已经得到了
//然后连接5得到newHead-1-2-3-4-5的过程
head.next.next = head;
// 5的next应该指向空
head.next = null;
return newHead;
}
}
递归总结:
- 理解递归函数的作用
- 写出递归中的递:也就是调用下一个
- 写出递归中的归:也就是调用下一个成功后,怎么操作让上一个成立
- 写出边界条件,避免死循环。
头插法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
// 头插法
ListNode newHead = null;
while (head != null){
//保存下一个节点,防止找不到
ListNode temp = head.next;
//插入
head.next = newHead;
newHead = head;
//原来的head指向下一个节点
head = temp;
}
return newHead;
}
}
5 .判断链表是否有环
快慢指针解决
代码
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head.next;
//fast为空的话,fast.next就会空指针,所有需要两个都不为空
while(fast != null && fast.next != null){
if (slow.val == fast.val) return true;
slow = slow.next;
fast = fast.next.next;
}
return false;
}
}
6.循环链表解决约瑟夫问题
public static void josephus(){
//循环链表
CircleLinkedList<Integer> list = new CircleLinkedList<>();
for(int i = 1; i <= 8; i++){
list.add(i);
}
//指向头节点
list.reset(); // current->1
while(!list.isEmpty()){
//这里定义走的步数
list.next();
list.next();
//删除节点
System.out.println(list.remove());
}
}
注:仅用于学习交流