文章目录
1 链表介绍
1.1 简单概述
除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个"结点"(如概述旁的图所示),表示线性表中一个数据元素。线性表的链式存储表示,有一个缺点就是要找一个数,必须要从头开始找起,十分麻烦。
- 链表是以节点的方式来存储的
- 内存上来看:链表存储空间不连续(不像数组)
- 逻辑上来看:链表属于线性结构
下面想一下链表的基本操作:
- 取数据
- 插入数据
- 删除数据
- 获取数据量
1.2 接口定义
根据这些定义一个接口
package study.wyy.struct.list;
/**
* @author by wyaoyao
* @Description
* @Date 2021/1/10 11:06 上午
*/
public interface List<T> {
/****
* 添加数据
* @param t
*/
void add(T t);
/****
* 指定位置添加数据
* @param index
* @param t
*/
void add(int index, T t);
/****
* 返回链表中数据个数
* @return
*/
int size();
/****
* 删除索引处数据
* @param index
*/
void remove(int index);
/****
* 删除指定数据
* @param t
*/
void remove(T t);
/*****
* 修改数据
* @param index
* @param t
*/
void set(int index,T t);
/****
* 获取指定索引位置的数据指定数据
* @param index
*/
T get(int index);
}
2 单链表
如何判断是最后一个节点:当前节点的next为null,没有指向下一个节点,就表示当前节点为最后一个节点
2.1 思路分析
添加数据分析
- 插入到最后
- 找到最后一个节点:next为null
- 将next指向当前插入的节点
- 指定位置插入
- 找到要插入的位置的前一个节点
- 前一个节点的next指向当前插入的节点
- 当前插入的节点指向
删除数据分析
我们提供了两种删除:一个根据索引位置删除,一个是根据数据删除,根据数据删除,相比根据索引位置删除,只需要找到数据所在索引位置即可,再根据索引位置删除:
- 找到索引所在的位置的前一个节点
- 将前一个节点的next指向要删除节点的的下一个节点
- 并将删除节点的next指向null
删除第一个节点,上面的逻辑就不能复用了,针对这情况,链表在设计的时候,大家通常会增加一个头节点(head),这个节点不存数据,它的next指向的是我们的第一个节点,这样删除第一个时候,就和上面逻辑可以保持一致
2.2 代码实现
节点定义
根据上面的分析:节点→两部分组成
- 数据:data
- 指向下一个节点的引用:next
final class Node<T>{
T data;
Node<T> next;
public Node(T data) {
this(data,null);
}
public Node(T data,Node<T> next) {
this.data = data;
this.next = next;
}
}
实现接口
package study.wyy.struct.list;
/**
* @author by wyaoyao
* @Description
* @Date 2021/1/10 3:51 下午
*/
public class MyLinkedList<T> implements List<T> {
// 记录链表中的数据个数
private int size = 0;
// 定义一个头结点
private Node<T> head = new Node<T>();
/*@Override
public void add(T t) {
// 尾部添加
// 0 构造节点
Node<T> newNode = new Node<>(t);
// 1 遍历找到最后一个节点:最后一个节点的判断标准是next为null
// 这里必须定义一个临时变量(引用)指向头结点,从头结点开始变量,头结点是不能动的,动了就改变链表的顺序了
Node<T> p = this.head;
while (true){
if(p.next == null){
// 找到了最后一个节点,此时p已经指向了最后一个节点
break;
}
// 没有找到,就进行后移
p = p.next;
}
// 2 此时p已经指向了最后一个节点
// 最后一个节点的next指向我们的新节点
p.next = newNode;
// size ++
size++;
}*/
@Override
public void add(T t) {
/*
// 改进,size-1的位置就是最后一个节点
// 0 构造新的节点
Node<T> newNode = new Node<>(t);
// 这里必须定义一个临时变量(引用)指向头结点,从头结点开始变量,头结点是不能动的,动了就改变链表的顺序了
Node<T> p = this.head;
// size-1的位置就是最后一个节点
for(int i = 0; i<size(); i++){
// 进行后移
p = p.next;
}
// 2 此时p已经指向了最后一个节点
// 最后一个节点的next指向我们的新节点
p.next = newNode;
// size ++
size++;
*/
// 在分析:上面的逻辑,完全和指定位置添加是一样的了
this.add(this.size(), t);
}
@Override
public void add(int index, T t) {
// 指定位置添加
// 0 构造新的节点
Node<T> newNode = new Node<>(t);
Node<T> p = this.head;
// 1 找到这个位置的前一个节点,比如 index = 2, 就要从head节点移动2(index)次,移动到index=1的位置
for (int i = 0; i < index; i++) {
// 进行后移
p = p.next;
}
// 此时p已经指到,前一个节点
// 新节点的next指向后一个节点(此时后一个节点就是p.next)
newNode.next = p.next;
// 再将前一个节点next指向新的节点
p.next = newNode;
this.size++;
}
@Override
public int size() {
return this.size;
}
@Override
public void remove(int index) {
// 校验一下index不能超出下界
if (index >= this.size() || index < -1) {
throw new IndexOutOfBoundsException("数组越界");
}
Node<T> p = this.head;
// 1 找到这个位置的前一个节点,// 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
for (int i = 0; i < index; i++) {
// 进行后移
p = p.next;
}
// 此时p已经指到前一个节点
// 将前一个节点的next指向要删除节点的的下一个节点 : p.next要删除节点, p.next.next 要删除节点的的下一个节点
Node<T> removeNode = p.next;
p.next = removeNode.next;
// 并将删除节点的next指向null
removeNode.next = null;
this.size--;
}
@Override
public void remove(T t) {
// 1 遍历找到要删除数据的索引位置
Node<T> p = this.head;
int index = 0;
// 用于标记是否找到要删除的数据
boolean flag = false;
for (int i = 0; i < this.size(); i++) {
Node<T> next = p.next;
if (t.equals(next.data)) {
index = i;
flag = true;
break;
}
// 后移p
p = p.next;
}
if(flag){
// 说明没有找到数据
// 2 删除指定索引位置的数据
remove(index);
}
}
@Override
public void set(int index, T t) {
// 校验一下index不能超出下界
if (index >= this.size() || index < -1) {
throw new IndexOutOfBoundsException("数组越界");
}
// 定义一个临时指针p,
Node<T> p = this.head;
// 移动p指向指定索引位置
// 比如index=2,就要移动index=2的节点 p->index=0->index=1->index=2,也就是移动3(index+1)次
for (int i = 0; i < index + 1; i++) {
p = p.next;
}
p.data = t;
}
@Override
public T get(int index) {
// 校验一下index不能超出下界
if (index >= this.size() || index < -1) {
throw new IndexOutOfBoundsException("数组越界");
}
// 定义一个临时指针p,
Node<T> p = this.head;
// 移动p指向指定索引位置的前一个(此时的p.next就是当前索引位置的节点)
// 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
for (int i = 0; i < index; i++) {
p = p.next;
}
return p.next.data;
}
public void reverse() {
// 0 定义一个新的头节点
Node<T> reverseHead = new Node<T>();
// 1 定义一个临时指针,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
Node<T> p = this.head.next;
// 2 记录当前节点的下一个节点,为了能继续寻找节点
Node<T> next = null;
// 2 移动p进行遍历: 不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
while (p != null){
// 记录当前节点的下一个节点,为了能继续寻找节点
next = p.next;
// 当前节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
p.next = reverseHead.next;
// reverseHead指向当前节点
reverseHead.next = p;
// p后移
p =next;
}
// 将head.next指向reverseHead的next
head.next = reverseHead.next;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node<T> p = this.head;
for (int i = 0; i < this.size(); i++) {
Node<T> next = p.next;
stringBuilder.append(next.data.toString());
if (i != size() - 1) {
stringBuilder.append(",");
}
// 后移p
p = p.next;
}
stringBuilder.append("]");
return stringBuilder.toString();
}
final class Node<T> {
T data;
Node<T> next;
public Node() {
}
public Node(T data) {
this(data, null);
}
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
测试:
package study.wyy.struct.list;
import sun.jvm.hotspot.utilities.Assert;
import javax.sound.midi.Soundbank;
/**
* @author by wyaoyao
* @Description
* @Date 2021/1/10 6:04 下午
*/
public class Test {
@org.junit.Test
public void testAdd(){
List<Integer> list = new MyLinkedList<>();
list.add(1);
list.add(3);
System.out.println(list);
list.add(1,2);
System.out.println(list);
list.add(4);
list.add(6);
System.out.println(list);
list.add(4,5);
System.out.println(list);
}
@org.junit.Test
public void tesGet(){
List<Integer> list = new MyLinkedList<>();
list.add(0);
list.add(1);
list.add(2);
Assert.that(0==list.get(0),"数据不对");
Assert.that(1==list.get(1),"数据不对");
Assert.that(2==list.get(2),"数据不对");
}
@org.junit.Test(expected=IndexOutOfBoundsException.class)
public void tesSet(){
List<Integer> list = new MyLinkedList<>();
list.add(0);
list.add(1);
list.add(2);
Assert.that(0==list.get(0),"数据不对");
Assert.that(1==list.get(1),"数据不对");
Assert.that(2==list.get(2),"数据不对");
list.set(2,3);
Assert.that(3==list.get(2),"数据不对");
list.set(0,1);
Assert.that(1==list.get(0),"数据不对");
list.set(1,2);
Assert.that(2==list.get(1),"数据不对");
// 数组越界
list.set(3,4);
}
@org.junit.Test
public void tesRemove(){
List<String> list = new MyLinkedList<>();
list.add("0");
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.remove(0);
// [1,2,3,4,5]
System.out.println(list);
list.remove(2);
// [1,2,4,5]
System.out.println(list);
list.remove("4");
//[1,2,5]
System.out.println(list);
}
}
2.3 扩展
2.3.1 单链表的反转
思路分析
大概的思路:
- 定义一个新的头节点
- 定义一个临时指针p,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
- 移动p进行遍历: 结束条件不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
- 遍历时要记录当前节点的下一个节点,为了能继续寻找节点
- 当前取下的节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
- reverseHead指向当前取下的节点
- p后移
- 将head.next指向reverseHead的next
public void reverse() {
// 0 定义一个新的头节点
Node<T> reverseHead = new Node<T>();
// 1 定义一个临时指针,指向第一个元素(在移动p的时候,p指向的的就是当前节点)
Node<T> p = this.head.next;
// 2 记录当前节点的下一个节点,为了能继续寻找节点
Node<T> next = null;
// 2 移动p进行遍历: 不是p.next不等于null,p.next==null,此时p还在最后一个节点,还需要后移,才不会漏掉最后一个节点
while (p != null){
// 记录当前节点的下一个节点,为了能继续寻找节点
next = p.next;
// 当前节点要指向前一个取下的节点,比如当前是第二个节点,这个节点的next要指向原先的第一个节点
p.next = reverseHead.next;
// reverseHead指向当前节点
reverseHead.next = p;
// p后移
p =next;
}
// 将head.next指向reverseHead的next
head.next = reverseHead.next;
}
测试
@org.junit.Test
public void test(){
MyLinkedList<String> list = new MyLinkedList<>();
list.add("0");
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
System.out.println(list);
list.reverse();
System.out.println(list);
}
3 双向链表
单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除 时节点,总是找到 temp,temp 是待删除节点的前一个节点
单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
双向链表的节点分为三部分
- next: 指向下一个节点
- pre: 指向后一个节点
- data: 数据域
3.1 思路分析
添加数据分析
添加到最后:
- 找到最后一个节点(next=null)
- 新插入节点的pre指向最后一个节点
- 最后一个节点的next指向新查询的节点
同样还是需要一个临时指针从头节点开始遍历寻找
指定位置添加
- 找到要插入位置的前一个节点
- 新节点的next指向后一个节点(newNode.next = p.next)
- 再将前一个节点next指向新的节点(p.next=newNode)
- 后一节点的pre指向当前的新节点(newNode.next.pre = newNode)
- 新节点的pre指向前一个节点 (newNode.pre = p)
删除数据分析
- 找到要删除的节点p
- p的前一个节点next指向p的后一个节点(p.pre.next = p.next)
- p的后一个节点的pre指向p的前一个节点(p.next.pre = p.pre)
注意:删除的就是最后一个节点呢(p.next =null),上面p.next.pre就会抛出空指针异常,这个情况是要特殊处理的
此时就没有后一个节点 - p.next=null,p.pre=null
3.3 代码实现
package study.wyy.struct.list;
/**
* 双向链表
*
* @author by wyaoyao
* @Description
* @Date 2021/2/27 5:16 下午
*/
public class MyDoubleLinkedList<T> implements List<T> {
// 记录链表中的数据个数
private int size = 0;
// 定义一个头结点
private Node<T> head = new Node<>();
@Override
public void add(T t) {
// 构造新的节点
Node<T> newNode = new Node<>(t);
// 定义一个临时指针,指向头结点,进行遍历
Node p = this.head;
while (true) {
if (p.next == null) {
// 找到了最后一个节点,此时p已经指向了最后一个节点
break;
}
// 后移p
p = p.next;
}
// 此时p已经指向了最后一个节点,最后一个节点的next指向我们的新节点
p.next = newNode;
// 新节点的pre指向p(也就是前一个节点)
newNode.pre = p;
this.size++;
}
@Override
public void add(int index, T t) {
// 0 构造新的节点
Node<T> newNode = new Node<>(t);
// 定义一个临时指针,指向头结点,进行遍历
Node p = this.head;
// 找到要插入位置的前一个节点
// 比如 index = 2, 就要从head节点移动2(index)次,移动到index=1的位置
for (int i = 0; i < index; i++) {
// 移动p
p = p.next;
}
// 此时p已经指到,前一个节点
// 新节点的next指向后一个节点(此时后一个节点就是p.next)
newNode.next = p.next;
// 再将前一个节点next指向新的节点
p.next = newNode;
// 后一节点的pre指向当前的新节点
newNode.next.pre = newNode;
// 新节点的pre指向前一个节点 (注意:p的赋值必须放到最后,p中间要是发生变化,就会导致节点顺序不对)
newNode.pre = p;
this.size++;
}
@Override
public int size() {
return this.size;
}
@Override
public void remove(int index) {
// 找到要删除的节点p
Node<T> p = getNode(index);
// p的前一个节点next指向p的后一个节点(p.pre.next = p.next)
p.pre.next = p.next;
// p的后一个节点的pre指向p的前一个节点(p.next.pre = p.pre)
// 注意:删除的就是最后一个节点呢(p.next =null),上面p.next.pre就会抛出空指针异常,这个情况是要特殊处理的
// 此时就没有后一个节点
if(null != p.next){
p.next.pre = p.pre;
}
this.size--;
}
@Override
public void remove(T t) {
// 1 遍历找到要删除数据的索引位置
Node<T> p = this.head;
int index = 0;
// 用于标记是否找到要删除的数据
boolean flag = false;
for (int i = 0; i < this.size(); i++) {
Node<T> next = p.next;
if (t.equals(next.data)) {
// 找到了
index = i;
flag = true;
break;
}
// 后移p
p = p.next;
}
if(flag){
// 说明没有找到数据
// 2 删除指定索引位置的数据
remove(index);
}
}
@Override
public void set(int index, T t) {
getNode(index).data = t;
}
@Override
public T get(int index) {
return getNode(index).data;
}
private Node<T> getNode(int index) {
// 校验一下index不能超出下界
if (index >= this.size() || index < -1) {
throw new IndexOutOfBoundsException("数组越界");
}
// 定义一个临时指针,指向头结点,进行遍历
Node<T> p = this.head;
// 移动p指向指定索引位置的前一个(此时的p.next就是当前索引位置的节点)
// 比如index=2,就要移动index=1的节点 p->index=0->index=1,也就是移动2(index)次
for (int i = 0; i < index; i++) {
p = p.next;
}
// 取出数据
return p.next;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("[");
Node<T> p = this.head;
for (int i = 0; i < this.size(); i++) {
Node<T> next = p.next;
stringBuilder.append(next.data.toString());
if (i != size() - 1) {
stringBuilder.append(",");
}
// 后移p
p = p.next;
}
stringBuilder.append("]");
return stringBuilder.toString();
}
final class Node<T> {
T data;
Node<T> next;
Node<T> pre;
public Node() {
}
public Node(T data) {
this(data, null, null);
}
public Node(T data, Node<T> next, Node<T> pre) {
this.data = data;
this.next = next;
this.pre = pre;
}
}
}
测试:
package study.wyy.struct.list;
import sun.jvm.hotspot.utilities.Assert;
/**
* @author by wyaoyao
* @Description
* @Date 2021/1/10 6:04 下午
*/
public class DoubleLinkedListTest {
@org.junit.Test
public void testAdd(){
List<Integer> list = new MyDoubleLinkedList<>();
list.add(1);
list.add(3);
// [1,3]
System.out.println(list);
list.add(1,2);
// [1,2,3]
System.out.println(list);
list.add(4);
list.add(6);
// [1,2,3,4,6]
System.out.println(list);
// [1,2,3,4,5,6]
list.add(4,5);
System.out.println(list);
}
@org.junit.Test
public void tesGet(){
List<Integer> list = new MyDoubleLinkedList<>();
list.add(0);
list.add(1);
list.add(2);
Assert.that(0==list.get(0),"数据不对");
Assert.that(1==list.get(1),"数据不对");
Assert.that(2==list.get(2),"数据不对");
}
@org.junit.Test(expected=IndexOutOfBoundsException.class)
public void tesSet(){
List<Integer> list = new MyDoubleLinkedList<>();
list.add(0);
list.add(1);
list.add(2);
Assert.that(0==list.get(0),"数据不对");
Assert.that(1==list.get(1),"数据不对");
Assert.that(2==list.get(2),"数据不对");
list.set(2,3);
Assert.that(3==list.get(2),"数据不对");
list.set(0,1);
Assert.that(1==list.get(0),"数据不对");
list.set(1,2);
Assert.that(2==list.get(1),"数据不对");
// 数组越界
list.set(3,4);
}
@org.junit.Test
public void tesRemove(){
List<String> list = new MyDoubleLinkedList<>();
list.add("0");
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.remove(0);
// [1,2,3,4,5]
System.out.println(list);
list.remove(2);
// [1,2,4,5]
System.out.println(list);
list.remove("4");
//[1,2,5]
System.out.println(list);
}