整理的笔记
【有错请见谅且指出,无错以后自己看的时候有新见解再添】
目录
资源
视频:https://www.bilibili.com/video/BV1iJ411E7xW?p=38
线性表
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。【举例:排队】
前驱元素 若A元素在B元素的前面,则称A为B的前驱元素
后继元素 若B元素在A元素的后面,则称B为A的后继元素
线性表的特征 数据元素之间具有一种 “ —对一 ” 的逻辑关系。
- 第一个数据元素没有前驱,这个数据元素被称为头结点;
- 最后一个数据元素没有后继,这个数据元素被称为尾结点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继
如果把线性表用数学语言来定义,则可以表示为(a1,…ai-1,ai,ai+1,…an),ai-1领先于ai,ai领先于ai+1,称ai-1是ai的前驱元素,ai+1是ai的后继元素
线性表的分类 线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
顺序表的实现
顺序表API设计:
//SequencetList.java
public class SequencetList<T>{
private T[] elements;//存储元素的数组
private int N;//记录当前顺序表中的元素个数
// 构造方法
public SequencetList(int capacity) {
//初始化数组
this.elements = (T[]) new Object[capacity];
//初始化长度
this.N = 0;
}
// 将一个线性表置为空表
public void clear() {
this.N = 0;
}
// 判断当前线性表是否为空表
public boolean isEmpty() {
return N == 0;
}
// 获取线性表的长度
public int length() {
return N;
}
// 获取指定位置的元素
public T get(int i) {
return elements[i];
}
// 向线性表中添加元素 t
public void insert(T t) {
elements[N++] = t; //添加一个元素后,N为1,elements[N]暂时是空的
}
// 在 i 元素处插入元素 t
public void insert(int i, T t) {
// 先把i索引处的元素及其后面的元素依次向后移动一位
// N所在的是空的,往后挪刚好一格一格的。
for (int index = N; index > i; index--) {
elements[index] = elements[index - 1];
}
// 再把t元素放到i索引处即可
elements[i] = t;
// 元素个数 + 1,让N指向最后一个元素后一个空
N++;
}
// 删除指定位置 i 处的元素,并返回该元素
public T remove(int i) {
// 记录索引i处的值
T current = elements[i];
// 索引i后面元素依次向前移动一位即可
for (int index = i; index < N - 1; index++) {
elements[index] = elements[index + 1];
}
// 元素个数 - 1
N--;
return current;
}
// 查找 t 元素第一次出现的位置
public int indexOf(T t) {
for (int index = 0; index < N; index++) {
if (elements[index].equals(t)) return index;
}
return -1;
}
}
//SequencetListTest.java
public class SequencetListTest{
public static void main(String[] args) {
//创建顺序表对象
SequencetList<String> s1 = new SequencetList<>(10);
//测试插入
s1.insert("111");
s1.insert("123");
s1.insert("8520");
s1.insert(3, "一三四");
//测试获取
String getResult = s1.get(3);
System.out.println("获取索引3处的结果为" + getResult);
//测试删除
String removeResult = s1.remove(0);
System.out.println("删除的元素为" + removeResult);
//测试清空
s1.clear();
System.out.println("清空后的线性表中的元素个数为" + s1.length());
}
}
顺序表的遍历(Java中特有)
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。
在java中,遍历集合的方式一般都是用的是foreach循环,如果想让我们的SequenceList也能支持foreach循环,则需要做如下操作:
- 让SequenceList实现Iterable接口,重写iterator方法;
- 在SequenceList内部提供一个内部类SIterator,实现Iterator接口,重写hasNext方法和next方法;
代码∶
//SequencetList.java 中添加
public class SequencetList<T> implements Iterable<T>{
//.............省略之前写的
@Override
public Iterator<T> iterator() {
return new SIterator();
}
//内部类
private class SIterator implements Iterator{
private int cusor;//遍历计数
public SIterator(){
this.cusor = 0;
}
@Override
public boolean hasNext() {
return cusor < N;//小于N证明还有元素,不小于就没有
}
@Override
public Object next() {
return elements[cusor++]; //获取元素值后指针后移
}
}
}
//SequencetListTest.java 中添加
public class SequencetListTest{
public static void main(String[] args) {
//创建顺序表对象
SequencetList<String> s1 = new SequencetList<>(10);
//测试插入
s1.insert("111");
s1.insert("123");
s1.insert("8520");
s1.insert(3, "一三四");
//中间如果空了一段,在循环时,只输出到null,后面的不输出
//例:s1.insert("111");s1.insert("123");s1.insert(3, "一三四");
//输出:
// 111
// 123
// null
//遍历
for (String s : s1) {
System.out.println(s);
}
}
}
顺序表的容量可变
问题:创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素。超过指定的大小时会报错然后不能插入了。(这种设计不符合容器的设计理念)
不可变时报 java.lang.ArrayIndexOutOfBoundsException 【数组索引越界异常】
解决:容量的伸缩性(实际就是改变存储数据元素的数组的大小)
需要考虑什么时候需要改变数组的大小?
-
添加元素时:
添加元素时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。 -
移除元素时:
移除元索时,应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存空间的浪费,应该创建一个容量更小的数组存储元索。如果我们发现数据元素的数量不足数组容量的1/4,则创建一个是原数组容量的1/2的新数组存储元素。
代码:
//SequencetList.java 中添加,修改insert和remove方法
//根据参数newSize,重置elements的大小
public void reSize(int newSize){
//定义一个临时数组,指向原数组
T[] temp = elements;
//创建新数组
elements = (T[])new Object[newSize];
//把原数组的数据拷贝到新数组即可
for (int i = 0; i < N; i++){
elements[i] = temp[i];
}
}
// 向线性表中添加元素 t
public void insert(T t) {
//调整大小
if (N == elements.length){
reSize(elements.length * 2);
}
elements[N++] = t; //添加一个元素后,N为1,elements[N]暂时是空的
}
// 在 i 元素处插入元素 t
public void insert(int i, T t) {
// 调整大小
if (N == elements.length){
reSize(elements.length * 2);
}
// 先把i索引处的元素及其后面的元素依次向后移动一位
// N所在的是空的,往后挪刚好一格一格的。
for (int index = N; index > i; index--) {
elements[index] = elements[index - 1];
}
// 再把t元素放到i索引处即可
elements[i] = t;
// 元素个数 + 1,让N指向最后一个元素后一个空
N++;
}
// 删除指定位置 i 处的元素,并返回该元素
public T remove(int i) {
// 记录索引i处的值
T current = elements[i];
// 索引i后面元素依次向前移动一位即可
for (int index = i; index < N - 1; index++) {
elements[index] = elements[index + 1];
}
// 元素个数 - 1
N--;
//调整大小
if (elements.length / 4 == N)
{
reSize(elements.length / 2);
}
return current;
}
// SequenceListTest2.java
public class SequenceListTest2 {
public static void main(String[] args) {
SequencetList<String> s1 = new SequencetList<>(2);
s1.insert("12345");
s1.insert("wefdvcdc");
s1.insert("强强强强");
s1.insert("的生产线");
for (String s : s1) {
System.out.println(s);
}
}
}
顺序表的时间复杂度
get():不难看出,不论数据元素量N有多大,只需要一次elements[i]就可以获取到对应的元素,所以时间复杂度为O(1);
insert(int i, T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);
remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。
Java中ArrayList实现
Java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。
-
是否用数组实现 【有】
-
有没有扩容操作【有】
-
有没有提供遍历方式【有】
要实现遍历,必然会实现Iterable这个接口,重写iterator方法。
重写了hasNext()和net()
Q: 为什么不直接使用ArrayList类,而要自己写呢?
A:
因为ArrayList类中考虑了各方面的因素(安全性,通用性),使得这个类的代码很臃肿。直接使用ArrayList可能使得效率不是很高。又或者说,因为具有通用性,不能解决一些特殊场景的问题。
自己会写的话,就能写出来更适合我们开发的实际问题所需要的数据结构【这也是为什么要学习数据结构和算法的所在】
链表
顺序表的查询很快【数组下标】,时间复杂度为O(1),但是增删的效率比较低,因为每一次增删操作都伴随着大量的数据元素移动【最差情况需要挪动整个表】。解决方案:使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
如何使用链表?
按照面向对象的思想,我们可以设计一个类,来描述结点这个事物,用一个属性描述这个结点存储的元素,用来另外—个属性描述这个结点的下一个结点。
结点类实现:
public class Node<T> {
public T item; //存储元素
public Node next; //指向下一个结点
//构造方法
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
生成链表:
public static void main(String[] args) throws Exception{
//构建结点 【结点之间并没有关系】
Node<Integer> first = new Node<Integer>(11, null);
Node<Integer> second = new Node<Integer>(13, null);
Node<Integer> third = new Node<Integer>(12, null);
Node<Integer> fourth = new Node<Integer>(8, null);
Node<Integer> fifth = new Node<Integer>(9, null);
//生成链表 【连接结点与结点】
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
}
单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
//LinkList.java
public class LinkList<T> implements Iterable<T> {
private Node head; //记录头结点
private int N; //记录链表的长度
//结点类(内部类)
private class Node {
T item; //存储数据
Node next; //下一个结点
//构造方法
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
//构造方法
public LinkList() {
//初始化头结点【不需要存数据,空链表不需要指向任何结点】
this.head = new Node(null, null);
//初始化元素个数
this.N = 0;
}
//清空链表
public void clear() {
//头结点不指向,元素个数为0,则清空了
head.next = null;
this.N = 0;
}
//获取链表的长度
public int length() {
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N == 0;
}
//获取指定位置i处的元素
public T get(int i) {
if (i > N)
return null;
//通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
Node n = head.next;
for (int index = 0; index < i; index++)
n = n.next; //不断把n向后变化
return n.item;
}
//向链表中添加元素t
public void insert(T t) {
//找到当前最后一个结点【n.next为null时,就到了最后一个结点】
Node n = head;
while (n.next != null)
n = n.next;
//创建新结点,保存元素t【最后一个结点不指向,为null】
Node newNode = new Node(t, null);
//让当前最后一个结点指向新结点
n.next = newNode;
//元素的个数+1
N++;
}
//向指定位置i处,添加元素t【新结点先指向后结点,要不然链断了】
public void insert(int i, T t) {
//找到i位置前一个结点
Node pre = head;
for (int index = 0; index <= i - 1; index++)
pre = pre.next;
//找到i位置结点
//Node node_i = pre.next;
//创建新结点,保存元素t,并让该结点指向原来i位置的结点
Node newNode = new Node(t, pre.next); //Node newNode = new Node(t,node_i)
//原来i位置的前一个结点指向新结点即可
pre.next = newNode;
//元素个数+1
N++;
}
//删除指定位置i处的元素,并返回被删除的元素
public T remove(int i) {
//找到i位置的前一个结点
Node pre = head;
for (int index = 0; index < i; index++)
pre = pre.next;
//找到i位置的结点
Node node_i = pre.next;
//找到i位置的下一个结点
//Node nextNode = node_i.next;
//前一个结点指向i位置后一个结点
pre.next = node_i.next; //pre.next = nextNode;
//元素个数-1
N--;
return node_i.item;
}
//查找元素t在链表中第一次出现的位置
//【从头结点开始,依次找到没一个结点,取出item和t比较,如果相同,就找到了】
public int indexOf(T t) {
Node n = head;
int index = 0;
do {
if (t.equals(n.next.item))
return index;
index++;
n = n.next;
}
while (n.next != null) ;
// if (t.equals(n.item))
// return 0;
// for (int i = 0; n.next != null; i++){
// if (t.equals(n.next.item))
// return i;
// n = n.next;
// }
return -1;
}
@Override
public Iterator<T> iterator() {
return new LIterator();
}
private class LIterator implements Iterator {
private Node n;
public LIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
n = n.next; //指针后移
return n.item;
}
}
}
//LinkListTest.java
public class LinkListTest {
public static void main(String[] args) {
//创建单向链表对象
LinkList<String> s1 = new LinkList<>();
//测试插入
s1.insert("111");
s1.insert("123");
s1.insert("8520");
s1.insert(3, "一三四");
//遍历
for (String s : s1) {
System.out.println(s);
}
System.out.println("----------------");
//测试获取
String getResult = s1.get(3);
System.out.println("获取索引3处的结果为" + getResult);
//测试删除
String removeResult = s1.remove(0);
System.out.println("删除的元素为" + removeResult);
//测试清空
s1.clear();
System.out.println("清空后的线性表中的元素个数为" + s1.length());
}
}
双向链表
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
//TowWayLinkList.java
public class TowWayLinkList<T> implements Iterable<T> {
private Node head; //首结点
private Node last; //最后一个结点
private int N; //链表的长度
//结点类
private class Node {
public T item; //存储数据
public Node pre; //指向上一个结点
public Node next; //指向下一个结点
public Node(T item, Node pre, Node next) {
this.item = item;
this.pre = pre;
this.next = next;
}
}
public TowWayLinkList() {
//初始化头结点和尾结点
this.head = new Node(null, null, null);
this.last = null;
//初始化元素个数
this.N = 0;
}
//清空链表
public void clear() {
this.head.next = null;
this.head.pre = null;
this.head.item = null;
this.last = null;
this.N = 0;
}
//获取链表长度
public int length() {
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N == 0;
}
//获取第一个元素
public T getFirst() {
if (isEmpty())
return null;
return head.next.item;
}
//获取最后一个元素
public T getLast() {
if (isEmpty())
return null;
return last.item;
}
//插入元素t
public void insert(T t) {
if (isEmpty()) {
//如果链表为空
//创建新的结点
Node newNode = new Node(t, head, null);
//让新结点称为尾结点
last = newNode;
//让头结点指向这个新结点
head.next = newNode;
} else {
//如果链表不为空
//创建新的结点
Node newNode = new Node(t, last, null);
//让当前的尾结点指向新结点(最后一个的next指向新结点)
last.next = newNode;
//让新结点成为尾结点
last = newNode;
}
//元素个数+1
N++;
}
//向指定位置i处插入元素t
public void insert(int i, T t) {
Node pre = head;
//找到i位置的前一个结点
for (int index = 0; index < i; index++) {
pre = pre.next;
}
//找到i位置的结点
Node curr = pre.next;
//创建新结点
Node newNode = new Node(t, pre, curr);
//让i位置的前一个结点的下一个结点变为新结点
pre.next = newNode;
//让i位置的前一个结点变为新结点
curr.pre = newNode;
//元素个数+1
N++;
}
//获取指定位置i处的元素
public T get(int i) {
Node n = head.next;
for (int index = 0; index < i; index++) {
n = n.next;
}
return n.item;
}
//找到元素t在链表中第一次出现的位置
public int indexOf(T t) {
Node n = head;
int index = 0;
while (n.next != null) {
n = n.next;
if (t.equals(n.item))
return index;
index++;
}
return -1;
}
//删除位置i处的元素,并返回该元素
public T remove(int i) {
//找到i位置的前一个结点
Node pre = head;
for (int index = 0; index < i; index++)
pre = pre.next;
//找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node nextNode = curr.next;
//让i位置的前一个结点的下一个结点变为i位置的下一个结点
pre.next = nextNode;
//让i位置的下一个结点的上一个结点变为i位置的前一个结点
nextNode.pre = pre;
//元素的个数-1
N--;
return curr.item;
}
//循环
@Override
public Iterator<T> iterator() {
return new TIterator();
}
private class TIterator implements Iterator {
private Node n;
public TIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
//TowWayLinkListTest.java
public class TowWayLinkListTest {
public static void main(String[] args) {
//创建双向链表对象
TowWayLinkList<String> s1 = new TowWayLinkList<>();
//测试插入
s1.insert("111");
s1.insert("123");
s1.insert("8520");
s1.insert(2, "一三四");
//遍历
for (String s : s1) {
System.out.println(s);
}
System.out.println("----------------");
//测试获取
String getResult = s1.get(3);
System.out.println("获取索引3处的结果为" + getResult);
//测试删除
String removeResult = s1.remove(0);
System.out.println("删除的元素为" + removeResult);
System.out.println("___________________");
System.out.println("第一个元素是:" + s1.getFirst());
System.out.println("最后一个元素是:" + s1.getLast());
//测试清空
s1.clear();
System.out.println("清空后的线性表中的元素个数为" + s1.length());
}
}
Java中LinkedList实现
java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法
-
底层是否用双向链表实现【是】
-
结点类是否有三个域【是】
链表的复杂度分析
get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)
insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);【仅插入,O(1)】
remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);【仅移除,O(1)】
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
链表反转【面试高频题目】 有点没懂
单链表的反转
需求:
原链表中数据为:1->2->3->4
反转后链表中数据为:4->3->2->1
反转APl:
public void reverse():对整个链表反转
public wode reverse(Node curr):反转链表中的某个结点curr,并把反转后的curr结点返回
使用递归可以完成反转,递归反转其实就是从原链表的第一个存数据的结点开始,依次递归调用反转每一个结点,直到把最后一个结点反转完毕,整个链表就反转完毕。
//LinkList.java
//用来反转整个链表
public void reverse() {
//递归调用反转单个结点
//判断当前链表是否为空链表,如果时空链表,则结束运行,如果不是,则调用重载的reverse方法完成反转
if (isEmpty())
return;
reverse(head.next);
}
//反转指定的结点curr,并把反转后的结点返回
public Node reverse(Node curr) {
//出口:没有下一个结点
if (curr.next == null) {
head.next = curr;
return curr;
}
//如果有下一个结点,就递归的反转当前节点curr的下一个结点
//返回值就是链表反转后,当前节点的上一个结点
Node pre = reverse(curr.next);
//让返回的结点的下一个结点变为当前结点curr
pre.next = curr;
//把当前结点的下一个结点变为null
curr.next = null;
return curr;
}
//LinkListTest2.java
public class LinkListTest2 {
public static void main(String[] args) {
//创建单向链表对象
LinkList<String> s1 = new LinkList<>();
//测试插入
s1.insert("111");
s1.insert("123");
s1.insert("8520");
s1.insert(1, "一三四");
//遍历
for (String s : s1) {
System.out.print(s + "\t");
}
System.out.println("----------------");
System.out.println("反转后:");
s1.reverse();
for (String s : s1) {
System.out.print(s + "\t");
}
}
}
快慢指针
快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍
中间值问题(非大小值,找链表最中间那个指针)
利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间节点的目的。
如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针。
//FastSlowTest.java
public class FastSlowTest {
public static void main(String[] args) {
Node<String> first = new Node<String>("aa", null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> sixth = new Node<String>("ff", null);
Node<String> seventh = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
//查找中间值
String mid = getMid(first);
System.out.println("中间值为:" + mid);
}
/**
* @param first 链表的首结点
* @return 链表的中间结点的值
*/
public static String getMid(Node<String> first) {
//定义两个指针
Node<String> fast = first;
Node<String> slow = first;
//使用两个指针遍历链表,当快指针指向的结点没有下一个结点了,就可以结束了
//结束之后,慢指针指向的结点就是中间值
while (fast != null && fast.next != null) {
//变化fast的值和slow的值
fast = fast.next.next;
slow = slow.next;
}
return slow.item;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
单向链表是否有环问题
使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。
//CircleListCheckTest.java
public class CircleListCheckTest {
public static void main(String[] args) {
Node<String> first = new Node<String>("aa", null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> sixth = new Node<String>("ff", null);
Node<String> seventh = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
//产生环
seventh.next = third;
//判断链表是否有环
boolean circle = isCircle(first);
System.out.println("first链表中是否有环:" + circle);
}
/**
* 判断链表中是否有环
* @param first 链表的首结点
* @return true为有环,false为无环
*/
public static boolean isCircle(Node<String> first) {
//定义两个指针
Node<String> fast = first;
Node<String> slow = first;
//使用两个指针遍历链表,如果快慢指针指向了同一个结点,那么证明有环
while (fast != null && fast.next != null) {
//变化fast的值和slow的值
fast = fast.next.next;
slow = slow.next;
//如果相等,那么指向的同一个
if (fast.equals(slow))
return true;
}
return false;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
有环链表入口问题
当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与"新"指针相遇的地方就是环的入口。证明这一结论牵涉到数论的知识,这里略,只讲实现。
//CircleListInTest.java 【无环的情况下,java.lang.NullPointerException】
public class CircleListInTest {
public static void main(String[] args) {
Node<String> first = new Node<String>("aa", null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> sixth = new Node<String>("ff", null);
Node<String> seventh = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
//产生环
seventh.next = third;
//查找环的入口结点
Node<String> entrance = getEntrance(first);
System.out.println("first链表中环的入口结点元素为:" + entrance.item);
}
/**
* 查找有环链表中环的入口结点
*
* @param first 链表的首结点
* @return 环的入口结点
*/
public static Node getEntrance(Node<String> first) {
//定义快慢指针
Node<String> fast = first;
Node<String> slow = first;
Node<String> temp = null; //临时指针,找到环时才指向首结点
//遍历链表,先找到环(快慢指针相遇)准备一个临时指针,指向链表的首结点,继续遍历
//直到慢指针和临时指针相遇;那么相遇时所指向的结点就是环的入口
while (fast != null && fast.next != null) {
//变化fast的值和slow的值
fast = fast.next.next;
slow = slow.next;
//判断快慢指针是否相遇,如果相等,那么指向的同一个,是个环
if (fast.equals(slow)) {
temp = first;
continue;
}
//让临时结点变化
if (temp != null) {
temp = temp.next;
//判断慢指针和临时指针是否相遇
if (temp.equals(slow))
break;
}
}
return temp;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
循环链表
循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null ,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
循环链表的构建:
//CircleList.java
public class CircleList {
public static void main(String[] args) {
//创建结点
Node<Integer> first = new Node<Integer>(1, null);
Node<Integer> second = new Node<Integer>(2, null);
Node<Integer> third = new Node<Integer>(3, null);
Node<Integer> fourth = new Node<Integer>(4, null);
Node<Integer> fifth = new Node<Integer>(5, null);
Node<Integer> sixth = new Node<Integer>(6, null);
Node<Integer> seventh = new Node<Integer>(7, null);
//构建单链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
//构建循环链表,让最后一个结点指向第一个结点
seventh.next = first;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
约瑟夫问题
问题描述:
传说有这样一个故事,在罗马人占领乔塔帕特后,39个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏。
问题转换:
41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。
-
编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
-
自退出那个人开始的下一个人再次从1开始报数,以此类推;
-
求出最后退出的那个人的编号。
图示:
解题思路:
-
构建含有41个结点的单向循环链表,分别存储1~41的值,分别代表这41个人;
-
使用计数器count,记录当前报数的值;
-
遍历链表,每循环一次,count++ ;
-
判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
//JosephTest.java
public class JosephTest {
public static void main(String[] args) {
//解决约瑟夫问题
//1.构建循环链表。包含41个结点,分别存储1-41之间的值
Node<Integer> first = null; //记录首结点
Node<Integer> pre = null; //记录前一个结点
for (int i = 1; i <= 41; i++) {
//插入结点方式
//1.如果是第一个结点
if (i == 1) {
first = new Node<>(i, null);
pre = first;
continue;
}
//2.如果不是第一个结点
/*
Node<Integer> newNode = new Node<>(i, null);
pre.next = newNode;
pre = newNode;
*/
pre.next = new Node<>(i, null);
pre = pre.next;
//如果是最后一个结点,那么需要让最后一个结点的下一个结点变为first,变为循环链表了
if (i == 41)
pre.next = first;
}
//2.需要count计数器,模拟报数
int count = 0;
//3.遍历循环链表
Node<Integer> n = first; //记录每次遍历拿到的结点,默认从首结点开始
Node<Integer> before = null; //记录当前结点的上一个结点,【方便删除】
//如果(n == n.next)删的只剩下一个,自己指向自己,无限循环
while (n != n.next) {
//模拟报数
count++;
//判断当前报的数是不是3
if (count == 3) {
//如果是3,就把当前结点删除掉,打印当前结点,重置count=0,让当前节点n后移
before.next = n.next;
System.out.print(n.item + ",");
count = 0;
n = n.next;
} else {
//如果不是3,让before变成当前结点,让当前结点后移
before = n;
n = n.next;
}
}
//打印最后一个元素
System.out.println(n.item);
}
//结点类
private static class Node<T> {
T item;//存储数据
Node next;//下一个结点
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}