线性表概述
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的优先序列
该数据结构的样子有点类似于排队
先来讲讲线性表的知识
若A元素在B元素的前面,则称A为B的前驱元素
若B元素在A元素的后面,则称B为A的后继元素
线性表的特征
首先第一个数据元素没有前驱元素,这个数据元素被称之为头结点
其次最后一个数据元素没有后继元素,这个数据元素被成为尾结点
除了第一个和最后一个元素外,其他数据元素有且仅有只有一个前驱元素与后继元素
用数学语言来表述则是如图所示
最后我们来讲讲线性表的分类
线性表数据存储的方式可以是顺序存储,也可以是链式存储,按照数据存储方式的不同,可以把线性表分为顺序表和链表。
顺序表
我们先来讲讲顺序表,首先我们应该先实现顺序表,顺序表在计算机内存中是以数组形式保存的线性表,现在我们来实现这个实现这个顺序表,先来看看其API设计
由API我们可以实现其代码如下
package algorithm.sort;
public class SequenceList<T> {
//存储元素的数组
private T[] eles;//该数组还没有进行初始化,没有赋予空间大小
//记录当前顺序表中的元素个数
private int N;
//构造方法
public SequenceList(int capacity){
//初始化数组
this.eles=(T[])new Object[capacity];
//之所以采用这种方式,是因为this.eles=new T[capacity];这种语法无法通过
//其原因在于泛型T无法被直接实例化
//初始化长度
this.N=0;
}
//将一个线性表置为空表
private void clear(){
this.N=0;
//个人认为这里只是将记录数组长度的变量赋值为0,并没有真正将数组的元素清空
}
//判断当前线性表是否为空表
public boolean isEmpty(){
return N==0;
}
//获取线性表的长度
public int length(){
return N;
}
//获取指定位置的元素
public T get(int i){
return eles[i];
}
//向线性表中添加元素t
public void insert(T t){
eles[N++]=t;
//将数值赋给对应位置之后,将长度+1
//同样我认为这方法只是将记录的值+1,实际上没有创建新的用于存放数据的空间
}
//在i元素处插入元素t
public void insert(int i,T t){
for (int index=N;index>i;index--){
eles[index]=eles[index-1];
}
//再把t元素放到i索引处即可
eles[i]=t;
//这里利用倒序遍历法来将数组插入后续的值都后移一位的思想值得学习
//但是我认为这里同样存在没有增加存放插入数据的空间的问题
//而且应该会报数组下标越界异常才对,但是实际运行没啥问题,就有点小怪
//元素个数+1
N++;
}
//删除指定位置i处的元素,并返回该元素
public T remove(int i){
//记录索引i处的值
T current = eles[i];
//索引i后面元素一次向前移动一位
for (int index=i;index<N-1;index++){
eles[index]=eles[index+1];
}
//元素个数-1
N--;
return current;
}
//查找t元素第一次出现的位置
public int indexOf(T t){
for (int i=0;i<N;i++){
if(eles[i].equals(t)){//使用equals方法进行比较
return i;
}
}
return -1;
}
}
虽然在这个代码的注释里我写了很多我认为的问题,但实际运行的时候,居然没啥问题,我猜想这是因为底层代码有对应处理机制的结果
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们要给我们的顺序表提供遍历方式。
值得一提的是,这里面的遍历是java特有的一种方式,即使我们不提供遍历,这个顺序表也是能够正常运行的,所以这一块的内容作为了解即可
接着我们来讲讲关于顺序表的遍历,首先我们来看看其遍历时我们应该实现的要求
接下来我们将遍历实现
package algorithm.sort;
import java.util.Iterator;
//若想要遍历,就应该要先实现Iterable接口
public class SequenceList<T> implements Iterable<T> {
//存储元素的数组
private T[] eles;//该数组还没有进行初始化,没有赋予空间大小
//记录当前顺序表中的元素个数
private int N;
//构造方法
public SequenceList(int capacity){
//初始化数组
this.eles=(T[])new Object[capacity];
//之所以采用这种方式,是因为this.eles=new T[capacity];这种语法无法通过
//其原因在于泛型T无法被直接实例化
//初始化长度
this.N=0;
}
//将一个线性表置为空表
private void clear(){
this.N=0;
//个人认为这里只是将记录数组长度的变量赋值为0,并没有真正将数组的元素清空
}
//判断当前线性表是否为空表
public boolean isEmpty(){
return N==0;
}
//获取线性表的长度
public int length(){
return N;
}
//获取指定位置的元素
public T get(int i){
return eles[i];
}
//向线性表中添加元素t
public void insert(T t){
eles[N++]=t;
//将数值赋给对应位置之后,将长度+1
//同样我认为这方法只是将记录的值+1,实际上没有创建新的用于存放数据的空间
}
//在i元素处插入元素t
public void insert(int i,T t){
//把i索引处的元素及其后面的元素依次向后移动一位
for (int index=N;index>i;index--){
eles[index]=eles[index-1];
}
//再把t元素放到i索引处即可
eles[i]=t;
//这里利用倒序遍历法来将数组插入后续的值都后移一位的思想值得学习
//但是我认为这里同样存在没有增加存放插入数据的空间的问题
//而且应该会报数组下标越界异常才对,但是实际运行没啥问题,就有点小怪
//元素个数+1
N++;
}
//删除指定位置i处的元素,并返回该元素
public T remove(int i){
//记录索引i处的值
T current = eles[i];
//索引i后面元素一次向前移动一位
for (int index=i;index<N-1;index++){
eles[index]=eles[index+1];
}
//元素个数-1
N--;
return current;
}
//查找t元素第一次出现的位置
public int indexOf(T t){
for (int i=0;i<N;i++){
if(eles[i].equals(t)){//使用equals方法进行比较
return i;
}
}
return -1;
}
//实现Iterable接口需要重写Iterator方法
@Override
public Iterator<T> iterator() {
return new SIterator();
}
//重写该方法要令其返回一个Iterator,但是Iterator是接口,无法直接new对象
//因此我们自己写一个内部类令其继承Iterator并重写其内部的方法
//这样就可以通过new这个类来达到返回一个Iterator的目的了
private class SIterator implements Iterator{
//定义一个指针用于遍历
private int cursor;
//定义一个构造方法,调用该方法初始化指针为0
public SIterator(){
this.cursor=0;
}
//继承Iterator需要重写hasNext和next两个方法
@Override
public boolean hasNext() {
return cursor<N;
//该方法用于判断指针指向的位置是否还有元素,因此使用cursor<N
}
@Override
public Object next() {
return eles[cursor++];
//该方法用于获取元素之后使指针指向下一位,因此是cursor++
}
}
}
但是我们现在创造的数组也有问题,一个经典问题就是我们的数组无法完成扩容,这样如果我们一开始规定了我们的容量为3,那么我们就只能够存储3个元素了,实际上,如果会自动扩容或者是会自动减容的话,会更加符合我们的思维习惯,因此我们应该要给我们的数组增加自动扩容/减容的机制
那么我们现在先来确定我们什么时候需要扩容,什么时候需要减容,以及要减容多少。我们不妨先确定如果我们增加元素时需要扩容,那么我们就扩容到其原来的二倍,删除元素时,如果剩余元素小于原数组的1/4,那么我们就将数组减少到其一半
那么我们可以实现其代码如下
package algorithm.sort;
import java.util.Iterator;
//若想要遍历,就应该要先实现Iterable接口
public class SequenceList<T> implements Iterable<T> {
//存储元素的数组
private T[] eles;//该数组还没有进行初始化,没有赋予空间大小
//记录当前顺序表中的元素个数
private int N;
//构造方法
public SequenceList(int capacity){
//初始化数组
this.eles=(T[])new Object[capacity];
//之所以采用这种方式,是因为this.eles=new T[capacity];这种语法无法通过
//其原因在于泛型T无法被直接实例化
//初始化长度
this.N=0;
}
//将一个线性表置为空表
private void clear(){
this.N=0;
//个人认为这里只是将记录数组长度的变量赋值为0,并没有真正将数组的元素清空
}
//判断当前线性表是否为空表
public boolean isEmpty(){
return N==0;
}
//获取线性表的长度
public int length(){
return N;
}
//获取指定位置的元素
public T get(int i){
return eles[i];
}
//向线性表中添加元素t
public void insert(T t){
if(N==eles.length){
resize(2*eles.length);
}
eles[N++]=t;
//将数值赋给对应位置之后,将长度+1
//同样我认为这方法只是将记录的值+1,实际上没有创建新的用于存放数据的空间
}
//在i元素处插入元素t
public void insert(int i,T t){
if(N==eles.length){
resize(2*eles.length);
}
//把i索引处的元素及其后面的元素依次向后移动一位
for (int index=N;index>i;index--){
eles[index]=eles[index-1];
}
//再把t元素放到i索引处即可
eles[i]=t;
//这里利用倒序遍历法来将数组插入后续的值都后移一位的思想值得学习
//但是我认为这里同样存在没有增加存放插入数据的空间的问题
//而且应该会报数组下标越界异常才对,但是实际运行没啥问题,就有点小怪
//元素个数+1
N++;
}
//删除指定位置i处的元素,并返回该元素
public T remove(int i){
//记录索引i处的值
T current = eles[i];
//索引i后面元素一次向前移动一位
for (int index=i;index<N-1;index++){
eles[index]=eles[index+1];
}
//元素个数-1
N--;
if(N<eles.length/4){
resize(eles.length/2);
}
return current;
}
//查找t元素第一次出现的位置
public int indexOf(T t){
for (int i=0;i<N;i++){
if(eles[i].equals(t)){//使用equals方法进行比较
return i;
}
}
return -1;
}
//根据参数newSize,重置eles的大小
public void resize(int newSize){
//定义一个临时数组,指向原数组
T[] temp=eles;
//创建新数组
eles=(T[])new Object[newSize];
//把原数组的数据拷贝到新数组即可
for(int i=0;i<N;i++){
eles[i]=temp[i];
}
}
//实现Iterable接口需要重写Iterator方法
@Override
public Iterator<T> iterator() {
return new SIterator();
}
//重写该方法要令其返回一个Iterator,但是Iterator是接口,无法直接new对象
//因此我们自己写一个内部类令其继承Iterator并重写其内部的方法
//这样就可以通过new这个类来达到返回一个Iterator的目的了
private class SIterator implements Iterator{
//定义一个指针用于遍历
private int cursor;
//定义一个构造方法,调用该方法初始化指针为0
public SIterator(){
this.cursor=0;
}
//继承Iterator需要重写hasNext和next两个方法
@Override
public boolean hasNext() {
return cursor<N;
//该方法用于判断指针指向的位置是否还有元素,因此使用cursor<N
}
@Override
public Object next() {
return eles[cursor++];
//该方法用于获取元素之后使指针指向下一位,因此是cursor++
}
}
}
最后我们来分析下该方法的时间复杂度,直接贴图吧
这些结论其实JavaSe里也学过了,这里算是对知识了解的进一步补充
ArrayList
java中ArrayList实现
java中ArrayList集合的底层也是一种顺序表,同样使用数组实现,也同样提供了增删改查的功能
通过查看ArrayList的源码,来确定这三件事
显然,这三个都是确定的,因为我们都已经知道了在工具类里我们是可以调用这些方法的,那自然他们也实现了这些功能。
那既然java里已经实现好了这些类,那我们为什么还要自己学一个呢?因为java中的工具类为了其通用性和安全性,总体而言是写得比较臃肿的,有1500多行代码,实际运行的时候可能其写好的数据结构不符合我们的需求,也可能发生效率过低的情况,因此我们要学习自己写这种数据结构,未来能够自己实现这种类型的数据结构来增强效率
链表
学习完顺序表之后,我们现在就来学习链表
我们之前说过了,顺序表底层其实是数组结构,而数据结构的缺点在于其增删元素时都容易涉及到数组的复制操作,这样效率就比较低,而链表结构则没有这个缺点,因此我们来学习链表这个数据结构
链表的插入与删除元素如图所示
这个JavaSe里已经学习过了,不多提
节点
接着我们来看看节点的API设计
可以看到其API里有一个构造方法,调用该构造方法需要传入一个数据类型,然后需要传入一个内存地址,该地址就是节点指向的下一个节点的内存地址
这里我们将next简单称之为指针,指向下一个指点
根据其API设计,我们可以实现其节点如下
根据上面这个节点的实现类,我们可以在主方法里这样生成链表
单向链表
接下来我们先学习单向链表,先来看看其结构
单向链表有多个结点,每个结点都由一个数据域和一个指针域构成,数据域用来存储数据,而指针域用来存储指向下一个结点的引用
值得一提的是链表的头结点不存储数据,其指针域指向第一个真正存储数据的结点
接下来我们看看其API设计
由API可以实现单项链表如下,这里我们还把遍历的方式也实现了
package algorithm.sort;
import java.util.Iterator;
//给链表提供遍历方式,因此要实现Iterable接口,要求重写iterator方法
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() {
//初始化头结点,头结点不存储数据,因此item是null,头结点刚初始化也不指向谁,因此next也是null
this.head=new Node(null,null);
//初始化元素个数
this.N=0;
}
//清空链表
public void clear() {
head.next=null;
this.N=0;
//清空链表,直接让头结点不指向下一个结点,这样结点由于没有指向,会被垃圾回收器回收,达到清空链表的目的
}
//获取链表的长度
public int length(){
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N==0;
}
//获取指定位置i处的元素
public T get(int i) {
//通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
Node n = head.next;//先创造一个结点n并赋予其头结点的引用
for (int index = 0; index < i; index++) {
n=n.next;//每次循环令n变为其结点的引用,循环i次就正好到自己想要到的结点的位置
}
return n.item;
}
//向链表中添加元素t
public void insert(T t) {
//先找到当前最后一个结点
Node n = head;//先定义一个结点n令其等于头结点
while (n.next!=null){
n=n.next;
//利用while循环来将n的next循环到指向结尾位置
}
//创建新结点,保存元素t
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-1次,正好到i位置的前一个结点
}
//找到i位置的结点
Node curr = pre.next;
//创建新结点,并且新结点要指向原来i位置的结点
Node newNode = new Node(t,curr);
//原来i位置的前一个结点指向新结点
pre.next=newNode;
//元素的个数+1
N++;
}
//删除指定位置i处的元素,并返回被删除的元素
//要删除指定位置的i处的元素,同样要先找到该元素的一个前后元素
public T remove(int i) {
//找到i位置的前一个结点
Node pre = head;
for (int index = 0; index <= i-1; index++) {
pre=pre.next;
}
//要找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node nextNode = curr.next;
//前一个结点指向下一个结点
pre.next = nextNode;
//元素个数-1
N--;
return curr.item;
}
//查找元素t在链表中第一次出现的位置
public int indexof(T t){
//从头结点开始,依次找到每一个结点,取出item和t比较,相同则返回下标
Node n = head;
for (int i = 0;n.next!=null; i++) {
//循环继续条件为n.next!=null,这样就可以达到遍历到底的效果
n=n.next;
if(n.item.equals(t)){
return i;
}
}
return -1;//代码执行到此说明链表里没有目标元素,因此返回-1
}
@Override
public Iterator<T> iterator() {
return new LIterator();
//该方法的重写要求返回一个iterator对象
//但是iterator是接口,无法直接创造对象
//因此创建一个内部类来实现iterator接口
//通过创建该内部类的方式来返回有相同作用的对象
}
//实现iterator接口需要重写hasNext和Next方法
private class LIterator implements Iterator{
private Node n;
public LIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
//该方法用于判断当前指针指向还有没有下一个元素
//用n.next!=null的判断代码可以通过判断其指针域是否为空来进行判断
}
@Override
public Object next() {
n = n.next;
return n.item;
//先让指针指向下一个结点,接着返回该结点的数据内容
}
}
}
通过测试发现这个代码没毛病
双向链表
接着我们来学习双向链表,先来看看双向链表的结构图
接着我们来看看双向链表的结点的API设计
接着来看看双向链表的API设计
由API我们可以设计对应方法如下,同样也已经先实现好了遍历方法
package algorithm.sort;
import java.util.Iterator;
//给链表提供遍历方式,因此要实现Iterable接口,要求重写iterator方法
public class TwoWayLinkList<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 TwoWayLinkList(){
//初始化头结点的尾结点
this.head = new Node(null,null,null);
this.last = null;//此时尾结点还不存在,因此赋予null
//初始化元素个数
this.N=0;
}
//清空链表
public void clear(){
//要实现清空链表的效果,只要做两件事情,第一件是令头结点不再指向任何结点
this.head.next=null;
this.head.pre=null;//这个可写可不写
this.head.item=null;//同上
//第二件是让最后一个尾结点为null,让尾结点不存储任何东西
this.last=null;
//最后让记录N的长度的变量为0
this.N=0;
}
//获取链表长度
public int length(){
return N;
}
//判断链表是否为空
public boolean isEmpty(){
return N==0;
}
//获取第一个元素
public T getFist(){
//先判断头结点是否为空,即头结点有无指向下一个结点
if (isEmpty()){
return null;//若为空则返回null
}
//若不为空则返回头结点的下一个结点的内容
return head.next.item;
}
//获取最后一个元素
public T getLast(){
//先判断头结点是否为空,即头结点有无指向下一个结点
if (isEmpty()){
return null;//若为空则返回null
}
//若不为空则返回尾结点的内容
return last.item;
}
//插入元素t
public void insert(T t){
//链表为空的情况
if(isEmpty()){
//先创建新的结点,该头指针域指向头结点,尾指针域为空
Node newNode = new Node(t,head,null);
//让新结点成为尾结点,这也是其尾指针域为空的原因
last=newNode;
//让头结点的尾指针域指向尾结点
head.next=last;
}else {
//如果链表不为空的情况
//先将为结点用oldLast来保存
Node oldLast = last;
//创建新的结点,让该结点的头指针域指向原先的尾结点
Node newNode = new Node(t,oldLast,null);
//让当前的尾结点的尾指针域指向新结点
oldLast.next=newNode;
//让新结点成为尾结点
last = newNode;
}
//元素个数+1
N++;
}
//向指定位置插入元素t
public void insert(int i,T t){
//找到i位置的前一个结点,先定义pre结点存储这个节点的引用
//将pre定义为head并循环i次可以定义到i位置的前一个结点
Node pre = head;
for (int index = 0; index < i; index++) {
pre=pre.next;
}
//手动找到i位置的结点,定义curr结点存储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){
//将n定义为首结点的下一个结点,循环i次能正好定义到位置处的结点
Node n = head.next;
for (int index = 0; index < i; index++) {
n=n.next;
}
//定义到i位置之后取出该位置的数据
return n.item;
}
//找到元素t在链表中第一个出现的位置
public int indexOf(T t){
Node n = head;
for (int i = 0; n.next!=null; i++) {
n=n.next;
if(n.next.equals(t)){
return i;
}
}
return -1;
}
//删除i位置的元素,并返回该元素
public T remove(int i){
//找到i位置的前一个结点
Node pre = head;
for (int index = 0; index < i; index++) {
pre=pre.next;
}
//找到i位置的结点,并用结点curr暂时保存其引用
Node curr = pre.next;
//找到i位置的下一个结点,并用nextNode保存其引用
Node nextNode = curr.next;
//让原先i位置的前一个结点的尾指针域指向i位置的下一个结点
pre.next=nextNode;
//让原先i位置的下一个结点的头指针域指向i位置的前一个结点
nextNode.pre=pre;
//元素的个数-1
N--;
//返回被删除的元素的数据内容
return curr.item;
}
//继承Iterable接口重写的Iterator方法
@Override
public Iterator<T> iterator() {
return new TIterator();
//返回以实现方式实现的内部类,其作用等同于Iterator
}
//实现Iterator接口的类要重写hasNext和next方法
private class TIterator implements Iterator{
private Node n;//先定义一个结点
public TIterator(){
this.n=head;
//令该结点获得head结点的引用
}
@Override
public boolean hasNext() {
return n.next!=null;
//只要其结点的尾指针域不为空则表示还有下一个
}
@Override
public Object next() {
n=n.next;//令n进入下一个结点
return n.item;//取出该结点的数据
}
}
}
双向链表在java中早已在LinkedList类里实现过了,我们现在来查看其源码,我们查看源码的目标有以下几点
当然,我们实际查看会发现真的有,没啥毛病。不同的是源码里是没有不存放数据的头结点的,而是直接用first和last来保存头结点和尾结点并利用这两者来进行增删,这里的具体过程在JavaSe里已经学过了,这里不再赘述,了解下就行了
最后我们照例来研究下其各种方法的时间复杂度,这里我们直接上结论吧
虽然说其时间复杂度都是O(n);但是链表数据结构涉及到增删时仍然比数组结构要好得多,主要是链表数据结构里不涉及元素的交换,而且不需要指定一块连续的空间,其主要花费的时间在于for循环的遍历来定位结点上
相比较于顺序表,链表的查询操作性能可能会比较低,因此当我们程序中查询操作比较多的时候,建议使用顺序表,而如果增删操作比较多,则建议使用链表
链表算法题
单链表的反转是面试中考试的一种高频题目,因此我们要学习单链表的反转
链表反转
先来看看我们的需求
当然,理论上如果我们只是想输入这么一个数据的话只要倒序遍历链表就可以达成我们的目的了,但是我们这里的目的是将链表的指向全部逆转过来,然后顺序遍历之后获得反转之后的结果
那我们应该如何去实现呢?先来看看其反转的API设计
我们再来看看其图示原理
我们先来搞明白这个方法的原理,这个方法本身是利用递归方法实现的,首先我们定义个reverse方法用于反转单向链表,该方法会先通过递归的方式优先进入到最后一个结点中,一旦我们到了最后一个结点,那么我们就要令head结点的尾指针域指向这个最后的结点,然后通过递归的反转不断地将每一个结点指向上一个结点,同时要消除原先上一个结点指向下一个结点的指针,最终完成反转
接下来我们可以实现其代码如下
package algorithm.sort;
import java.util.Iterator;
//给链表提供遍历方式,因此要实现Iterable接口,要求重写iterator方法
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() {
//初始化头结点,头结点不存储数据,因此item是null,头结点刚初始化也不指向谁,因此next也是null
this.head=new Node(null,null);
//初始化元素个数
this.N=0;
}
//清空链表
public void clear() {
head.next=null;
this.N=0;
//清空链表,直接让头结点不指向下一个结点,这样结点由于没有指向,会被垃圾回收器回收,达到清空链表的目的
}
//获取链表的长度
public int length(){
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N==0;
}
//获取指定位置i处的元素
public T get(int i) {
//通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
Node n = head.next;//先创造一个结点n并赋予其头结点的引用
for (int index = 0; index < i; index++) {
n=n.next;//每次循环令n变为其结点的引用,循环i次就正好到自己想要到的结点的位置
}
return n.item;
}
//向链表中添加元素t
public void insert(T t) {
//先找到当前最后一个结点
Node n = head;//先定义一个结点n令其等于头结点
while (n.next!=null){
n=n.next;
//利用while循环来将n的next循环到指向结尾位置
}
//创建新结点,保存元素t
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-1次,正好到i位置的前一个结点
}
//找到i位置的结点
Node curr = pre.next;
//创建新结点,并且新结点要指向原来i位置的结点
Node newNode = new Node(t,curr);
//原来i位置的前一个结点指向新结点
pre.next=newNode;
//元素的个数+1
N++;
}
//删除指定位置i处的元素,并返回被删除的元素
//要删除指定位置的i处的元素,同样要先找到该元素的一个前后元素
public T remove(int i) {
//找到i位置的前一个结点
Node pre = head;
for (int index = 0; index <= i-1; index++) {
pre=pre.next;
}
//要找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node nextNode = curr.next;
//前一个结点指向下一个结点
pre.next = nextNode;
//元素个数-1
N--;
return curr.item;
}
//查找元素t在链表中第一次出现的位置
public int indexof(T t){
//从头结点开始,依次找到每一个结点,取出item和t比较,相同则返回下标
Node n = head;
for (int i = 0;n.next!=null; i++) {
//循环继续条件为n.next!=null,这样就可以达到遍历到底的效果
n=n.next;
if(n.item.equals(t)){
return i;
}
}
return -1;//代码执行到此说明链表里没有目标元素,因此返回-1
}
@Override
public Iterator<T> iterator() {
return new LIterator();
//该方法的重写要求返回一个iterator对象
//但是iterator是接口,无法直接创造对象
//因此创建一个内部类来实现iterator接口
//通过创建该内部类的方式来返回有相同作用的对象
}
//实现iterator接口需要重写hasNext和Next方法
private class LIterator implements Iterator{
private Node n;
public LIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
//该方法用于判断当前指针指向还有没有下一个元素
//用n.next!=null的判断代码可以通过判断其指针域是否为空来进行判断
}
@Override
public Object next() {
n = n.next;
return n.item;
//先让指针指向下一个结点,接着返回该结点的数据内容
}
}
//用于反转整个单向链表的方法
public void reverse(){
//先判断当前链表是否为空,若为空则结束运行
if(isEmpty()){
return;
}
//程序运行到此说明不为空,执行用于反转链表的重载reverse方法
reverse(head.next);
}
//反转指定的结点curr,并把反转后的结点返回
public Node reverse(Node curr) {
//若结点为最后一个结点,即该结点不再指向下一个结点
if(curr.next==null){
//则让首结点指向该结点,该结点是原先的尾结点
head.next=curr;
//返回该尾结点
return curr;
}
//递归返回的当前结点用定义好的pre结点对象来进行保存
Node pre = reverse(curr.next);
//让返回的结点的尾指针域指向原先的上一个结点
//相当于是加上了后面对前面的指针
pre.next=curr;
//把当前结点的尾指针域赋值为null
//相当于是把原先指向下一个结点的指针给清除
curr.next=null;
//最后按照需求返回当前的结点,这个返回只是为了不报错而设计的
//设计在程序里这个返回值用户可以选择接收也可以选择不接收
return curr;
}
}
174-193行的代码就是我们添加的用于反转的代码,里面已经有了不少用于解释的注释了,这里不多提
快慢指针
接着我们来学习快慢指针,快慢指针可以解决链表的三个问题,分别是中间值问题、单向链表是否有环问题和有环链表入口问题。
我们通过自己定义两个快慢指针,以此来制造出自己想要的差值,这个差值可以找到我们链表上对应的结点,一般来说,快指针的移动步长为慢指针的两倍
我们先来解决中间值问题,假设我们现在有一个单向链表,我们的需求是我们传入链表的首结点,我们就能够找到其中间值,那么我们应该怎么利用快慢指针来实现需求呢?请看代码
package algorithm.sort;
public class test {
public static void main(String[] args) {
Node<String> first = new Node<>("aa",null);
Node<String> second = new Node<>("bb",null);
Node<String> third = new Node<>("cc",null);
Node<String> fourth = new Node<>("dd",null);
Node<String> fifth = new Node<>("ee",null);
Node<String> six = new Node<>("ff",null);
Node<String> seven = new Node<>("gg",null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//查找中间值
String mid = getMid(first);
System.out.println("中间值为:"+mid);
//中间值为:dd
}
/**
*
* @param first 链表的首结点
* @return 链表的中间结点的值
*/
public static String getMid(Node<String> first) {
//先定义两个指针,两指针的起始位置都是first
Node<String> fast = first;
Node<String> slow = first;
//使用两指针遍历链表,快指针到最后一个结点时返回慢指针指向的结点
while (fast!=null&&fast.next!=null) {
//防止出现空指针异常所以在循环里多加了一个fast不为空的判断
//接着定义快慢指针,快指针走两步慢指针走一步
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;
}
}
}
接下来我们来解决单向链表中是否有环的问题,我们先来看看有环与无环的单向链表
我们只要定义两个快慢指针令其遍历数组,并在遍历过程中不断判断两指针是否相等就可以确定是否有环,因为如果是无环单向链表,则快指针永远都不可能和慢指针重合,但如果是有环单向链表,那么随着循环的不断进行,快指针必然会与慢指针重合,通过这种方法来判断其是否有关,
同样的,我们这里也是要求我们只要传入首结点,就能够找出链表是否有环,我们可以构造代码如下
package algorithm.sort;
public class test {
public static void main(String[] args) {
Node<String> first = new Node<>("aa",null);
Node<String> second = new Node<>("bb",null);
Node<String> third = new Node<>("cc",null);
Node<String> fourth = new Node<>("dd",null);
Node<String> fifth = new Node<>("ee",null);
Node<String> six = new Node<>("ff",null);
Node<String> seven = new Node<>("gg",null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//产生环
seven.next = third;
//调用判断链表是否有环的方法
boolean circle = isCircle(first);
System.out.println("first链表中是否有环?"+circle);
//true
}
public static boolean isCircle(Node<String> first) {
//定义快慢指针
Node<String> fast = first;
Node<String> slow = first;
//遍历链表,如果快慢指针指向了同一个结点,那么证明有环
while (fast!=null&&fast.next!=null){
//防止空指针异常而特别设置的循环条件
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;
}
}
}
有环链表的入口问题
如果我们已经确定了一个链表有环,那么我们应该如何确定这个链表的环的入口呢?我们只要在快慢指针第一次相遇的时候定义一次新的慢指针,当这个慢指针和原来的慢指针相遇时所指的结点就是链表的环的入口了。这里的原理涉及到了数论的知识,我们不多做提及,反正这么做就对了的
那么我们可以实现其代码如下
package algorithm.sort;
public class test {
public static void main(String[] args) {
Node<String> first = new Node<>("aa",null);
Node<String> second = new Node<>("bb",null);
Node<String> third = new Node<>("cc",null);
Node<String> fourth = new Node<>("dd",null);
Node<String> fifth = new Node<>("ee",null);
Node<String> six = new Node<>("ff",null);
Node<String> seven = new Node<>("gg",null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//产生环
seven.next = third;
//调用判断链表是否有环的方法
Node<String> entrance = getEntrance(first);
System.out.println("first链表中环的入口的结点元素为"+entrance.item);
//cc
}
public static Node getEntrance(Node<String> first) {
//定义快慢指针
Node<String> fast = first;
Node<String> slow = first;
Node<String> temp = null;
//先将慢指针定义为null,存在但还不赋值
//遍历链表,如果快慢指针指向了同一个结点,那么证明有环
while (fast!=null&&fast.next!=null){
//防止空指针异常而特别设置的循环条件
fast = fast.next.next;
slow = slow.next;
//判断快慢指针是否相遇
if(fast.equals(slow)){
//一旦相遇就定义新的慢指针
temp = first;
continue;
//跳过下面的内容执行
//让temp指针在这一轮只定义不行动
}
//设置循环条件,让慢指针进行移位动作
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;
}
}
}
循环链表问题
那么到此为止我们就已经学习完了快慢指针了,接着我们来学习下链表里的循环链表的问题,先来看看循环链表的演示
简而言之就是将最后一个结点的尾指针域指向头结点,将头结点的引用赋给最后一个结点的尾指针域就完了,因为太简单所以代码都省略了
约瑟夫问题
接着我们来学习解决链表的一个经典的约瑟夫问题,我们先来看看什么是约瑟夫问题吧
接着我们可以把这个问题转换成链表的形式
我们再来看看其图示
接着我们来详细讲讲其具体的实现原理,首先我们需要构建这样一个循环链表,这个循环链表有41个结点,尾结点指向头结点。接着我们应该要模拟报数,每次报到3就将报数为3的位置的结点删除,然后其下一个结点要继续从1开始报数这样不断轮回,直到只剩下最后一个人
那么现在我们来看看我们的代码实现
package algorithm.sort;
public class test {
public static void main(String[] args) {
//解决约瑟夫问题
//1.构建包含41个结点的循环链表,分别储存1~41之间的值,储存的值可以视为每个人的编号
//用于记录首结点的结点,先赋值为null,因为我们还没有创建任何结点
Node<Integer> first = null;
//用于记录前一个结点,同样赋值为null,因为我们的首结点也没有前一个结点
Node<Integer> pre = null;
//用for循环来创建41个结点
for (int i = 1; i <= 41; i++) {
//之所以采用i=1的形式来创建,是因为我们的编号要求是从1开始的
//如果是第一个结点的情况
if(i==1){
//是第一个节点我们就直接创建新结点并将该结点赋予first,此时first成为了首结点
first = new Node<>(i,null);
//让pre记录first结点,便于下面的新结点的创建
pre = first;
continue;
}
//如果不是第一个结点的情况
//先创建一个新结点
Node<Integer> newNode = new Node<>(i,null);
//将上一个结点的尾指针域指向新结点
pre.next = newNode;
//让新创建的结点的引用赋给pre,让我们可以调用pre来代表这个节点
pre = newNode;
/*这里我们事先定义的pre结点变量的作用就显现出来了,正是因为我们定义了这个
* pre变量,因此在非第一个结点的创建的中,我们可以通过重复调用pre的方式
* 来达到让上一个结点的尾指针域指向新结点的目的,如果我们不定义pre变量
* 的话,那么我们就无法做到这个动作,定义一个变量来记录上一个变量,以此来
* 实现让上一个变量的指向下一个对象的指向动作,这个思想值得我们学习
* 那我们应该什么时候就需要自己去定义一个变量来储存前一个对象呢?我觉得
* 我们可以暂时简单理解为如果我们需要重复进行指向动作的话,那么我们就应该
* 创建一个变量来保存上一个结点,便于我们后续的重复指向*/
//如果是最后一个结点,那么我需要令其尾指针域指向头指针
if(i==41) {
pre.next=first;
}
}
//2.定义count计数器用于模拟报数
int count = 0;
//3.不断遍历循环链表,进行游戏
//记录每次遍历过程中拿到的结点,默认从首结点开始,这是自然,因为我们的游戏就是从编号1的人开始报数的
//可以简单理解为指向报数的人的指针
Node<Integer> n = first;
//记录当前结点的上一个结点,其主要作用就是为了完成链表内结点的指向,没有遍历前先赋值为null
Node<Integer> before = null;
/*构建while循环用于遍历,之所以我们的结束条件写为while是因为当我们的代码到最后一个
结点时,我们的代码会令最后一个结点指向自身,当指向自身时,就说明了此时只剩下约瑟夫了
。这时就是已经符合我们要求了的,所以我们的程序执行到此就可以结束了,因此我们的循环
结束条件是n!=n.next;即只要n不是指向自身,我们就继续进行遍历
*/
while (n!=n.next){
//模拟报数
count++;
//判断当前报数是否为3
if(count==3){
//若为3,
// 则删除当前结点并打印被删除结点,重置count并且让当前结点n后移一位
before.next = n.next;//删除当前结点
System.out.print(n.item+",");//打印被删除结点
count=0;//重置count
n=n.next;//让当前结点n后移
}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;
}
}
}
栈
那么学习完顺序表与链表之后,现在我们来学习栈的数据结构,在古代,栈其实就是客栈,基本作用是等用户来吃饭睡觉,酒足饭饱之后,再离开客栈,这是现实生活里的客栈
接着我们来看看计算机语言中的栈
栈是一种基于先进后出(FILO)的数据结构,FILO是First in last out的缩写,也就是先进后出的英文缩写。我们称数据进入栈的动作为压栈,数据从栈中出去的动作为弹栈
简单讲解完原理之后,我们来看看栈的API设计
首先我们要明确一点,栈是一种逻辑层面的数据结构,他将数据存储到计算机内部,其是要用到物理存储结构来存储数据的,而物理存储结构分为顺序存储结构和链式存储结构,也就是数组或者链表。这里我们采用链表的形式来去实现,课后如果自己有想法可以用数组的形式自己去实现下
由API设计我们可以实现栈的代码如下,注意这里我们已经实现了其遍历方法了
package algorithm.sort;
import java.util.Iterator;
//给链表提供遍历方式,因此要实现Iterable接口,要求重写iterator方法
public class Stack<T> implements Iterable{
//记录首结点
private Node head;
//栈中元素的个数
private int N;
private class Node{
public T item;
public Node next;
public Node(T item,Node next) {
this.item=item;
this.next=next;
}
}
//栈的构造方法,调用该方法将创造一个数据内容为null的结点对象并将引用传递给head
public Stack() {
this.head = new Node(null,null);
this.N=0;
}
//判断当前栈中元素的个数是否为0
public boolean isEmpty(){
return N==0;
}
//获取栈中元素的个数
public int size(){
return N;
}
//把t元素压入栈
/*
*该方法的原理是不断往链表里增加元素,如果增加的结点为1,那么就
* 让新结点被首结点指向,如果不为1,则让首结点指向该新结点,同时
* 该新结点指向原来的旧结点。这里就模仿了栈的数据的先进后出特点,
* 我们只要按照上述方式来构建链表,那么等到我们遍历时,我们的链表
* 所遍历的元素会先从最后一个加入的元素遍历到我们第一个加入的元素
*/
public void push(T t){
//找到首结点指向的第一个结点,并将其赋予给oldFirst
//私以为这里有问题,运行时会发生空指针异常
//但实际却没有发生空指针问题,我感到无法理解
Node oldFirst = head.next;
//创建新结点
Node newNode = new Node(t,null);
//让首结点指向新结点
head.next = newNode;
//让新结点指向原来被首结点指向的旧结点
newNode.next=oldFirst;
//元素个数+1
N++;
}
//弹出栈顶元素
public T pop(){
//找到首结点指向的第一个结点,并将该结点用oldFirst保存
Node oldFirst = head.next;
//如果oldFirst为null则直接返回null,不必再做判断
//这里代表的意思是已经取到底了,没有元素可以取了
if(oldFirst==null){
return null;
}
//让首结点指向原来的第一个结点的下一个结点
head.next=oldFirst.next;
//元素个数-1
N--;
return oldFirst.item;
}
@Override
public Iterator iterator() {
return new SIterator();
}
private class SIterator implements Iterator{
private Node n;
public SIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n=n.next;
return n.item;
}
}
}
那么到此为止,我们栈的数据结构就讲完了,接着我们来看看关于栈的一些应用案例
栈数据结构最简单的应用案例就是括号匹配问题
我们先来简单看看括号匹配问题的描述
那么我们应该怎么去解决这种问题呢?其实我们的思路很简单,我们只要对这个字符串进行遍历,如果我们检测到左括号,那么我们就将其压栈,如果我们检测到右括号,我们就进行弹栈,如果弹栈弹出的结点不为空,就说明右括号有左括号为之对应,如果为空则说明有括号不对应,那么我们直接返回false。遍历完之后我们检查栈中还有没有元素,若没有则说明该括号匹配,若有则说明还有括号是没有对应的右括号进行匹配的,这时我们也返回false
那么我们可以构建代码如下
package algorithm.sort;
public class test {
public static void main(String[] args) {
String str = "(())";
System.out.println(isMatch(str));//true
}
public static boolean isMatch(String str){
//1.创建栈对象,用于存储左括号
Stack<String> chars = new Stack<>();
//2.从左往右遍历字符串
for (int i = 0; i < str.length(); i++) {
String currChar = str.charAt(i)+"";
//在char类型后加个空字符串能够自动将该类型转换成String类型
//3.判断当前字符是否为左括号,若是则将该左括号入栈
if(currChar.equals("(")){
chars.push(currChar);
}else if(currChar.equals(")")){
//继续判断其是否为右括号,若是则进行弹栈
String pop = chars.pop();
//判断弹栈的内容是否为null,是则说明没有对应的左括号,直接返回false
if(pop==null){
return false;
}
}
}
//5.判断栈中还有没有剩余的左括号,若有则说明括号不匹配
if(chars.size()==0){
return true;
}else {
return false;
}
}
}
逆波兰表达式
刚刚我们用栈解决了括号的匹配问题,那么现在我们就要学习如何用栈数据结构来解决逆波兰表达式的计算问题,要想搞明白什么是逆波兰表达式,我们要先搞清楚什么是中缀表达式,且看中缀表达式的定义
中缀表达式的特点是两个运算符总是放在操作数的中间,这是我们人类计算最爱使用的运算表达式,但是对于计算机而言,这种表达式要进行大量的优先级相关操作,因此对于计算机来说,中缀表达式是不友好的,因此在计算机语言里我们要使用逆波兰表达式来代替中缀表达式
现在让我们来看看逆波兰表达式的定义
逆波兰表达式的运算原理在于将二元运算符前面两个值进行运算然后生成一个新的值,接着如果后续还有二元运算符,那么就拿这个新的值与其最靠近二元运算符的值进行运算,讲起来可能很不明朗,直接举例就行了
比如对于abc-d +而言,是先进行b-c,设结果为p,那么继续进行pd的运算,结果设为q,最后再进行a+q的运算,最后就能得到结果
对于abc-d+而言,先进行b-c的运算,设为p,再进行ap的运算,设为q,最后进行q+d的运算,最后就能得到结果
那么接下来如果我们要设置一个方法用于计算逆波兰表达式的值,那我们应该怎么构建我们的代码呢?
我们可以先遍历整个逆波兰表达式,判断每一个字符是否为运算符,若不是则入栈,若是则弹出两个操作数,然后计算这两个操作数之后再将这两个操作数的结果进行入栈,就这样不断进行遍历,到最后栈中必然剩余一个总和结果,那个结果就是我们的逆波兰表达式的值
请看原理图
按照原理图,我们可以构建代码如下
package algorithm.sort;
public class test {
public static void main(String[] args) {
String[] notation = {"3","17","15","-","*","18","6","/","+"};
System.out.println(caculate(notation));//9
}
public static int caculate(String[] notaion){
//1.定义一个栈,用于储存操作数
Stack<Integer> oprands = new Stack<>();
//2.遍历逆波兰表达式,得到每一个元素
for (int i = 0; i < notaion.length; i++) {
//取出逆波兰表达式里的对应值并保存在字符串curr中
String curr = notaion[i];
//定义三个用于取出栈中元素和保存其计算值的Integer变量
//之所以定义在这里是因为在Switch里不能重复定义
//如果在Switch里定义的话会因为重复定义而报错
Integer o1,o2,result;
switch (curr){
//用Switch来判断其对应的二元运算符
case "+":
//若为运算符则弹出两个操作数,完成运算后将结果压入栈中
o1 = oprands.pop();//弹栈并保存于o1
o2 = oprands.pop();//弹栈并保存于o2
result = o2 + o1;//进行计算
oprands.push(result);//将结果压栈
break;
case "-":
o1 = oprands.pop();//弹栈并保存于o1
o2 = oprands.pop();//弹栈并保存于o2
result = o2 - o1;//进行计算
oprands.push(result);//将结果压栈
break;
case "*":
o1 = oprands.pop();//弹栈并保存于o1
o2 = oprands.pop();//弹栈并保存于o2
result = o2 * o1;//进行计算
oprands.push(result);//将结果压栈
break;
case "/":
o1 = oprands.pop();//弹栈并保存于o1
o2 = oprands.pop();//弹栈并保存于o2
result = o2 / o1;//进行计算
oprands.push(result);//将结果压栈
break;
default:
//若为操作数,则将该操作数压栈
oprands.push(Integer.parseInt(curr));
//调用Integer.parseInt方法将字符串内容
//转换成Integer内容,用于存放
}
}
//最后弹出栈中的最后一个元素,其就为逆波兰表达式的结果
return oprands.pop();
}
}
那么到此为止,我们就实现了逆波兰表达式的计算代码了,栈的内容也讲完了
队列
现在我们来学习队列这一数据结构
首先我们要知道队列是一种基于先进先出(FIFO),FIFO是First in first out的简写,是一种只能在一端进行插入,在另一端进行删除的特殊线性表,我们直接来看看其图示说明
接下来我们来看看其API设计
同栈一样,队列也是可以通过链表或者是数组的方式来实现的,这里我们采用链表的方式来实现,那么我们可以构建其代码如下,注意,这里我们的队列也是已经实现了遍历方法的
package algorithm.sort;
import java.util.Iterator;
//给链表提供遍历方式,因此要实现Iterable接口,要求重写iterator方法
public class Queue<T> implements Iterable{
//记录首结点
private Node head;
//用于记录最后一个结点
private Node last;
//记录队列中元素的个数
private int N;
private class Node{
public T item;
public Node next;
public Node(T item,Node next) {
this.item=item;
this.next=next;
}
}
public Queue() {
this.head = new Node(null,null);
this.last = null;
this.N = 0;
}
//判断队列是否为空
public boolean isEmpty(){
return N==0;
}
//返回队列中的元素个数
public int size(){
return N;
}
/*
* 我们这里的这个方法的主要作用是向队列中插入元素t,那当我们是队列
* 数据结构时,我们应该怎么实现插入方法呢?我们可以按顺序在链表中
* 添加元素,每次添加都让新添加的元素成为尾结点
*/
public void enqueue(T t){
//先判断队列中有无元素,通过last是否为null来判断
if(last==null){
//如果当前尾结点为null,也就是队列为空
//直接将last定位到新创建的结点上,此时可以理解为新结点成为了尾结点
last = new Node(t,null);
//首结点指向尾结点
head.next=last;
}else {
//当前尾结点的last不为null,此时先将last的结点记录于oldLast上
Node oldLast = last;
//令last指向新的尾结点
last = new Node(t,null);
//让上一个结点的尾指针域指向尾结点
oldLast.next=last;
}
//元素个数+1
N++;
}
//从队列中拿出一个元素
/*
* 这个方法的主要作用是从队列中拿出一个元素,但是注意在队列的数据
* 结构中拿出元素是要按照先进先出的原则的,因此我们要想办法在取出
* 元素时做到先取出第一个放置的元素。我们可以先取出首结点的下一个
* 结点,然后让首结点指向被取出结点的下一个结点,按照这个方式取出
* ,我们就可以达到先进先出的效果
*/
public T dequeue(){
//先判断队列是否为空
if (isEmpty()){
return null;
//若为空则直接返回null
}
//将头结点指向的下一个结点用oldFirst记录起来
Node oldFirst = head.next;
//令头结点指向被取出结点的下一个结点,这个动作相当于删除结点同时下一个结点称为新结点
head.next = oldFirst.next;
N--;//元素个数-1
//这个动作其实是在删除元素,如果队列的元素被删除完了,那么要重置last
if(isEmpty()){
last=null;
}
return oldFirst.item;//返回被删除元素的数据
}
@Override
public Iterator iterator() {
return new QIterator();
}
private class QIterator implements Iterator{
//定义一个记录头结点的结点变量
private Node n;
public QIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
那么至此,我们队列也讲完了,那么线性表这一章节就算是讲完了
符号表
之前我们以及学习完线性表了,我们能够知道线性表里的元素每次都是存储了一个元素的,但是在现实生活中,我们的很多时候不只是需要存储一个元素,有时候是需要存储两个对应的元素的,而符号表就能满足我们的这种需求
符号表内的数据我们称之为键值对,有一个键key,和一个值value组成的,我们可以根据键来查找对应的值,符合表中,键具有唯一性,既不能出现重复的键
键值对在生活中的应用场景是非常广泛的,最简单的就比如说查找,假设书籍编号就是key,value是书名,那么我们就可以通过key来查找书名,非常简单。这只是一个最简单的应用的举例,实际上还有很多例子这里就不再赘述了
接下来我们来看看其API设计
节点类
首先是结点类的API设计
由于采用键值对的方式进行储存,因此其用于储存元素的结点类也不尽相同。再来看看符号表的API设计
无序符号表
那么根据上面的API设计我们可以实现其代码如下,我们这里采用链表的方式来进行实现
package algorithm.sort;
import java.util.Iterator;
public class SymbolTable<Key,Value> {
//记录首结点
private Node head;
//记录符号表中元素的个数
private int N;
//符号表中的结点类
private class Node{
//键,此处的Key是泛型标识
public Key key;
//值,此处的Value是泛型标识
public Value value;
//下一个结点
public Node next;
public Node(Key key,Value value,Node next) {
this.key=key;
this.value=value;
this.next=next;
}
}
//符号表的构造方法
public SymbolTable() {
//创建不存储任何元素的首结点,其主要作用是指向下一个结点
this.head = new Node(null,null,null);
this.N=0;
}
//获取符号表中键值对的个数
public int size(){
return N;
}
/*
*要实现该方法要先进行传入的key是否已经在符号表中存在的判断,如果
* 已经存在了那么我们就替换对应value的值就可以了。如果不存在那么
* 就正常插入,插入的方式不是让尾结点指向新结点,而是让头结点指向
* 新的结点,让新的结点指向原先首结点指向的下一个结点,有点类似于
* 队列中的插入方式,将新结点插入到链表的头部
*/
//往符号表中插入键值对
public void put(Key key,Value value) {
//先进行符号表中有无该key的键值对的循环判断
Node n = head;//用n保存首结点便于后续遍历
while (n.next!=null){//只要不为空说明还没循环到底,继续循环
//令n进入其原先所指向的下一个结点,由于n是首结点,所以
//其先进入下一个结点没有问题,因为首结点只负责指向
n = n.next;
//判断n结点存储的key与传入的key是否相等
if(n.key.equals(key)){
//若相等则替换对应结点内的value的值
n.value = value;
return;//插入完毕,直接结束该方法
}
}
//代码执行到此说明不存在与其相等的key,则创建新结点
Node newNode = new Node(key,value,null);
//用oldFirst保存首结点指向的第一个结点
Node oldFirst = head.next;
//令新结点指向首结点指向的下一个结点
newNode.next = oldFirst;
//让首结点指向新结点
head.next = newNode;
}
/*
* 该删除方法的实现原理是先找到对应的结点,然后将对应结点的上一个
* 结点指向对应结点的下一个结点
*/
//删除符合表中键为key的键值对
public void delete(Key key){
//先构建循环找到键为key的结点
Node n = head;//同样先创建n保存头结点
while (n.next!=null){
//判断n结点的下一个结点是否为key,若是就进入方法
//由于n最初是头结点,因此先构建代码n.next没有问题
if(n.next.key.equals(key)){
//令被删除结点的上一个结点的尾指针域指向被删除结点
//的下一个结点
n.next = n.next.next;
N--;
return;
}
//若不是则令n进入下一个结点
n = n.next;
}
}
//从符号表中获取key对应的值
public Value get(Key key){
//找到键为key的结点
Node n = head;
while (n.next!=null){
//令n进入其所指向的下一个结点
n = n.next;
//判断是否是与传入的key的值相同的结点
if(n.key.equals(key)){
//若是则返回该结点的value
return n.value;
}
}
//代码执行到此说明符号表内压根没有对应的key,返回null
return null;
}
}
但其实,我们上面所实现的符号表,它是没有序的。因为我们插入元素的时候,我们只是将其暴力地直接查到头结点处,而没有进行判断,如果我们想要实现有序符号表的话,那么我们应该要重新改造我们的put方法,让put方法可以做到插入时排序,使得插入完毕之后我们的符号表就处于有序的状态。
有序符号表
那我们应该怎么做到呢?我们先来看看实现该方法的原理演示图
我们让我们的key值与链表中的元素互相比较,直到发现比我们要插入的key值要大的元素,我们就让我们的要插入的结点指向该结点,再让该结点的上一个结点指向我们要插入的结点,接着消除原先上一个结点对下一个结点的指向就完了
那我们现在来实现下这个代码
package algorithm.sort;
import java.util.Iterator;
//令key继承Comparable接口,给Key所代表的元素提供比较方式,便于实现排序
public class OrderSymbolTable<Key extends Comparable<Key>,Value> {
//记录首结点
private Node head;
//记录符号表中元素的个数
private int N;
//符号表中的结点类
private class Node{
//键,此处的Key是泛型标识
public Key key;
//值,此处的Value是泛型标识
public Value value;
//下一个结点
public Node next;
public Node(Key key,Value value,Node next) {
this.key=key;
this.value=value;
this.next=next;
}
}
//符号表的构造方法
public OrderSymbolTable() {
//创建不存储任何元素的首结点,其主要作用是指向下一个结点
this.head = new Node(null,null,null);
this.N=0;
}
//获取符号表中键值对的个数
public int size(){
return N;
}
/*
*要实现该方法要先进行传入的key是否已经在符号表中存在的判断,如果
* 已经存在了那么我们就替换对应value的值就可以了。如果不存在那么
* 就正常插入,插入的方式不是让尾结点指向新结点,而是让头结点指向
* 新的结点,让新的结点指向原先首结点指向的下一个结点,有点类似于
* 队列中的插入方式
*/
//往符号表中插入键值对
public void put(Key key,Value value) {
//定义两个Node变量分别记录当前结点与当前结点的上一个结点
Node curr = head.next;//记录当前结点
Node pre = head;//记录当前结点的上一个结点
while (curr!=null&&key.compareTo(curr.key)>0){
/*
*构建while循环进行判断,要求当curr不为null且curr
* 的key值小于我们进行比较的传入的key值,满足条件
* 则说明结点定位还没到我们想要的传入值不小于结点key值
* 的那个位置,那么我们就让结点继续前进
*/
pre = curr;//令记录当前结点的上一个结点的结点前进一位
curr = curr.next;//令当前结点前进一位
}
//代码执行到此说明已经到达了我们想要的传入值不小于结点key值
//的位置,但是我们还要进行判断其是否相等
if(curr!=null&&key.compareTo(curr.key)==0){
curr.value = value;//如果相等则替换value
return;//结束方法
}
//代码执行到此说明传入值比小于结点值key
//创建新结点,令该结点指向其key值小于它的第一个结点
Node newNode = new Node(key,value,curr);
//令原先curr的结点的上一个结点指向这个新结点
pre.next = newNode;
//元素的个数+1
N++;
}
/*
* 该删除方法的实现原理是先找到对应的结点,然后将对应结点的上一个
* 结点指向对应结点的下一个结点
*/
//删除符合表中键为key的键值对
public void delete(Key key){
//先构建循环找到键为key的结点
Node n = head;//同样先创建n保存头结点
while (n.next!=null){
//判断n结点的下一个结点是否为key,若是就进入方法
//由于n最初是头结点,因此先构建代码n.next没有问题
if(n.next.key.equals(key)){
//令被删除结点的上一个结点的尾指针域指向被删除结点
//的下一个结点
n.next = n.next.next;
N--;
return;
}
//若不是则令n进入下一个结点
n = n.next;
}
}
//从符号表中获取key对应的值
public Value get(Key key){
//找到键为key的结点
Node n = head;
while (n.next!=null){
//令n进入其所指向的下一个结点
n = n.next;
//判断是否是与传入的key的值相同的结点
if(n.key.equals(key)){
//若是则返回该结点的value
return n.value;
}
}
//代码执行到此说明符号表内压根没有对应的key,返回null
return null;
}
}