数据结构与算法
1、稀疏数组
1.1二维数组回顾
使用
@Test
public void test(){
//声明 [3][4] 代表此二维数组三行4列
int[][] array = new int[3][4];
//array.length代表的是二维数组的行数 这里就是3
//array[i].length代表列数
int num = array.length;
for (int i = 0; i < array.length; i++) {
//array[i].length 表示的是第i行的列数 这里是4
for (int j = 0; j < array[i].length; j++) {
array[i][j] = i;
}
}
//遍历二维数组 一行一行遍历
//外层循环控制行
for (int i = 0; i < array.length; i++) {
//内存循环控制这一行的每一列
for (int j = 0; j < array[i].length; j++) {
System.out.println(array[i][j]);
}
}
}
理解
可以将二维数组看做是一个Map<T,List<T>>
,Map的key可以看做是行号,value就是这一行的数据(一维数组
),所以说通过行号找到的就是一个一维数组,或者可以看做是一个List集合
1.2稀疏数组的应用场景及解决方案
使用场景
当一个数组中大部分元素都为同一个值时,可以使用稀疏数组保存该数组
如何解决
- 稀疏数组首先记录原数组的
行数
,列数
,以及需要存储到稀疏数组的有效值的个数
- 然后记录出每一个值的
行数
列数
以及该值
如下图所示
左边原数组,右边稀疏数组需要记录的数据
稀疏数组还是一个二维数组
1.2稀疏数组编码实现
1.2.1二维数组转稀疏数组
思路分析
1、根据稀疏数组的定义,我们要先确定稀疏数组的大小即行和列,列数是确定的就是3,但是行要根据原始数组中的数据个数来决定,所以第一步我们要先遍历原数组找出原数组中的有效数据个数n,然后n+1就是稀疏数组的行数
,第一行记录的是原始数组的信息。
2、稀疏数组的大小以及原始数组的有效数据个数我们确定下来后,我们需要根据有效数据在原始数组中的位置,来为稀疏数组中的元素赋值,所以就得for循环遍历原始数组,判断有效数据然后记录有效数据的位置以此为稀疏数组赋值
编码实现
@Test
public void 二维数组转稀疏数组实现() throws IOException {
//原数组 定义0是无效数据 其他数据都是有效数据
int oriniArray[][] = {
{
0, 0, 0}, {
0, 2, 0}, {
0, 1, 0}};
//找出原数组中有效数据的个数
int num = 0;
for (int i = 0; i < oriniArray.length; i++) {
for (int j = 0; j < oriniArray[i].length; j++) {
if (oriniArray[i][j] != 0) {
num++;
}
}
}
// 根据有效数据的个数 声明稀疏数组的大小 并为第一行赋值
//第一行分别是 原数组的 行数 列数 有效数据个数
int[][] sparseArr = new int[num + 1][3];
sparseArr[0][0] = oriniArray.length;
sparseArr[0][1] = oriniArray[0].length;
sparseArr[0][2] = num;
//遍历原数组 为稀疏数组赋值
int index = 0;
//为稀疏数组的后面行赋值
for (int i = 0; i < oriniArray.length; i++) {
for (int j = 0; j < oriniArray[i].length; j++) {
//判断某个数据是有效数据 拿到这个数据的 (行 列 数据值)为稀疏数组赋值
if (oriniArray[i][j] != 0) {
index++;
sparseArr[index][0] = i;
sparseArr[index][1] = j;
sparseArr[index][2] = oriniArray[i][j];
}
}
}
}
1.2.2稀疏数组转二维数组
思路分析
- 第一步:直接读取稀疏数组第一行的数据,分别可以得到原数组的行和列,然后声明原数组
- 第二步 :直接遍历稀疏数组,拿到每一行的数据,直接为原数组对应索引位置的元素赋值即可
@Test
public void 稀疏数组转二维数组实现() {
//稀疏数组
int sparseArray[][] = {
{
3, 3, 2}, {
1, 1, 2}, {
2, 1, 1}};
//根据稀疏数组第一行的数据声明原数组
int[][] oringinArray = new int[sparseArray[0][0]][sparseArray[0][1]];
//遍历稀疏数组,根据每一行数据直接为原数组指定所有位置的元素赋值
for (int i = 1; i < sparseArray.length; i++) {
for (int j = 0; j < sparseArray.length; j++) {
oringinArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
}
}
2、队列(数组实现)
2.1队列概述
- 队列是一个有序列表,可以用
数组
或者链表
实现 - 特点:先进先出的原则
2.2数组模拟队列思路1
- 创建一个类表示队列,然后声明一个数组类型的成员变量存储数据,
- 然后设置三个变量,rear指向最后一个元素(实际存的就是最后一个元素的下标),front初始时指向第一个元素的前面,每次元素入队时rear指针后移,元素出队时front指针后移。
rear和front的初始值都是-1
,maxSize表示数组的最大容量, - 当rear=maxSize-1时(即rear指针已经指向了最后一个元素的位置),表示队满,当rear=front时,表示队空(可能是初始或者是元素全部出队)
2.3代码实现1
public class ArrayQueue {
private int front;
private int rear;
private int maxSize;
private int[] data;
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
data = new int[this.maxSize];
front = -1; //指向队列头部 即指向队列头的前一个位置(初始)
rear = -1; //指向队列尾部 指向队列尾部的数据 就是队列最后一个数据
}
//判断度队列是否满 判断rear是否指向最后一个元素
public boolean isFull() {
return rear == maxSize - 1;
}
//判断度队列是否满
public boolean isEmpty() {
return rear == front;
}
//取出数据 front指针后移指向元素(初始为-1) 然后获取指针指向的位置的元素
public int getDataFromQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
//front后移
front++;
return data[front];
}
//添加数据 先判断队列是否满 然后rear指针后移,将数据插入到rear指向的地方
public void setDataToQueue(int element) {
if (isFull()) {
throw new RuntimeException("队列已满");
}
rear++;
data[rear] = element;
}
//显示队列中的数据
public void showData() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
for (int i = 0; i < data.length; i++) {
System.out.println(data[i]);
}
}
//显示队头的数据
public int headQueue(){
if (isEmpty()){
throw new RuntimeException("队列为空");
}
return data[front+1];
}
}
2.4存在的问题
主要的问题就是出队的位置无法复用
上面我们在取出数据时front指针后移,一旦指针后移,那么front前面的位置就无法再次插入数据,所以这种方式这个数组只能使用一次==(即这个队列就成了一次性队列)==,接下来我们要改进成一个环形数组(核心就是%)
,实现即使数据已经出队front指针后移,但是我们仍然可以继续在已经出队的位置插入数据
2.5环形队列概述
环形队列也是一个数组,只是指针发生变化
front指针现在初始指向队列的第一个元素,即初始值就是0
rear指针指向元素的后一个位置,初始值也是0,
留一个空位置,用于区分判断堆满和队空
操作
入队
rear = (rear + 1) % maxSize;
移动rear指针,因为rear初始指向第一个位置,环形队列我们要求rear指向元素的后一个位置,所以rear+1,因为是环形的(并且最后一个位置的元素是空的,无法放数据),所以说我们还要%maxSize
,因为当前面的元素出队时,我们可以继续使用前面的位置,当rear指向最后一个位置时,这时+1刚好等于maxSize,%运算
刚好又回到了0的位置,则可以继续的进行入队操作,实现了重复利用
出队
front = (front + 1) % maxSize;
元素出队只是指针的变化,我们只需要移动front指针,因为front初始在0的位置,所以我们移动以后它指向了后一个元素,(而普通的队列元素出队时front指向的还是这个位置,改进后的这个好处就是当后面的位置满时,但是可以判断队列未满,因为前面的一个元素已经出队,指针已经后移了)
队空
rear == front
如何两个指针重合了,那么说明队列没有元素
队满
(rear + 1) % maxSize == front
判断队满,就是当尝试再次入队一个元素时,即rear指针移动一次时是否跟front重合,如果重合了说明队列满了(最后一个位置忽略)
元素的个数
//加maxSize防止rear-front为负数(即元素出队后又有元素入队时的一种情况)
(rear - front + maxSize) % maxSize
2.6环形队列代码实现
public class CircleArrayQueue {
private int front;
private int rear;
private int maxSize;
private int[] data;
public CircleArrayQueue(int maxSize) {
this.maxSize = maxSize;
//初始front指向第一个位置
front = 0;
//rear初始也指向第一个位置
rear = 0;
data = new int[maxSize];
}
//判断为空
public boolean isEmpty() {
return rear == front;
}
//判断满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
//添加数据
public void addData(int element) {
if (isFull()) {
throw new RuntimeException("队列已满");
}
//因为rear刚开始指向的就是0所以直接插入
data[rear] = element;
//rear后移 循环队列+1后必须取模 当到最后时前面元素出队那么这个位置还能继续使用
rear = (rear + 1) % maxSize;
}
public int getData() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
//先取出原位置数据
int element = data[front];
//指针后移 取模
front = (front + 1) % maxSize;
//返回原数据
return element;
}
//找出队列中的有效数据的个数
public int size() {
return (rear - front + maxSize) % maxSize;
}
//遍历队列中的有效数据个数
public void showData() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
for (int i = front; i < size(); i++) {
//可能下标越界 所以使用%从头找
System.out.println(data[i % maxSize]);
}
}
//返回队列的头元素
public int headData() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return data[front];
}
}
3、链表(线性结构)
3.1概述
- 链表是链式结构,是以节点的方式存储数据的,
在内存中的存储不一定是连续的
, - 链表中的每个节点最少包括
data域(存数据)
和next域(指向下一个节点)
- 普通链表增删改的效率比数组高,查询效率低
- 链表分为带头结点的和不带头结点的
3.2带头结点的单向链表操作
3.2.1定义
由上面的定义,我们知道链表是以节点的方式存储数据的,每一个节点至少有data域和一个next域,
下面我们将一个对象作为data域
data域
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Hero {
//唯一标识
private Integer id;
private String name;
private Integer age;
private Character gender;
}
节点
@Data
public class HeroNode {
//next域 还是一个HeroNode类型 指向下一个节点
public HeroNode next;
//data域
private Hero data;
//一个带参构造器,传入节点的data域
public HeroNode(Hero data) {
this.data = data;
}
}
链表类,用于维护各个节点,并定义节点的增删改查
要定义一个头结点
public class HasHeadLinkedList {
//头结点 data域为null
private HeroNode head = new HeroNode(null);
....增删改查方法
}
3.2.2遍历节点
遍历是链表操作的最重要的,因为想要找到某一个节点必须一个个的遍历找然后判断
单链表的话,遍历可以使用while循环,我们知道最后一个节点的next域是null,所以可以使用这个条件进行判断,因为带有头结点,有头结点我们才能找到每一个节点,所以头结不能进行操作,必须使用一个节点表示头结点
public void showData() {
//用一个temp节点表示头结点 操作这个temp节点
HeroNode temp = head;
//判断如果头结点的下一个节点为null,说明链表为空
if (temp.next == null) {
System.out.println("链表为空");
}
//while循环进行判断节点的下一个节点是否是null,不是null移动指针到下一个节点,循环进行判断,当找到某一个节点的下一个节点是null,说明这个节点是最后一个节点,结束循环,这样可以遍历链表中的每一个节点
while (temp.next != null) {
//指针移动
temp = temp.next;
Hero data = temp.getData();
System.out.println(data);
}
}
3.2.3添加节点(直接在节点的最后添加)
思路分析
在最后一个节点后添加一个节点,所以我们需要找到最后一个节点,就得先遍历整个链表,找到某一个节点的next为null,那么这个节点就是最后一个节点,然后直接将最后一个节点指向要添加的节点即可
- 遍历找到最后一个节点
- 最后一个节点指向新的节点
代码实现
public void addNode(HeroNode node) {
//用temp表示头节点
HeroNode temp = head;
//遍历链表,找到最后一个节点
while (temp.next != null) {
temp = temp.next;
}
//经过遍历之后,此时temp就是最后一个节点,直接将temp节点指向添加的节点即可
temp.next = node;
}
3.2.4插入节点_在一个节点的前面插入
思路分析
要想在某一个节点C前面插入一个节点B,我们首先要找到这个节点B的前面的节点A,(因为如果我们的指针指向C的话,那么只能完成B指向C,而A无法指向B,所以我们要找到C的前一个节点A,而我们找到A的话,可以通过A表示C(A.next))
所以将B指向C,然后A指向B即可完成
- 遍历链表,找到目标节点C的前一个节点A
- 将B指向C,A指向B即可
代码实现
public void insertBeforeNode(HeroNode oldNode, HeroNode newNode) {
HeroNode temp = head;
//遍历链表
while (temp.next != null) {
//判断某一个节点的下一个节点是否是目标节点
if (temp.next == oldNode) {
//将新节点指向目标节点
newNode.next = oldNode;
//目标节点的前一个节点指向新节点
temp.next = newNode;
//结束循环
break;
}
//指针后移 不符合条件继续向后遍历
temp = temp.next;
}
}
3.2.6插入节点_根据id插入节点
问题:
现有id为4 5 2 3 1**(data域中的某一数据)**的节点顺序加入到链表中,要求添加时按照id顺序进行添加 即最终链表的节点为 1->2->3->4->5
思路分析
默认链表是空的,第一个节点插入时直接可以插入,第二个节点插入时就必须判断它的id和第一个节点的关系,如果比第一个节点id大就插入到它的后面,比第一个节点id小就插入到前面,此时链表中就有了两个节点,且这两个节点是有序的。
然后后来的节点就有三种情况,在遍历整个链表的过程中,发现新来的节点的id
1、如果比第一个节点的id小那么直接插入到第一个节点的前面即可
2、如果在遍历的过程中发现这个新节点id刚好在两个相邻
的节点的id范围内,那么就插入到这两个节点之间。
3、如果比最后一个节点的id大那么直接插入到最后一个节点的后面即可
比如上面的插入顺序是4 5 2 3 1,4直接插到第一个(4->)
,然后5比4大插到4后面(4->5)
,然后2比4小直接插到第一个位置即4前面(2->4->5)
,3刚好在2和4之间,直接插到2 4之间 (2->3->4->5)
,1比2小直接插到2前面(1->2->3->4->5)
代码实现
public boolean insertByHeroId(HeroNode node) {
Integer id = node.getData().getId();
HeroNode temp = head;
//第一次插入的情况 链表为空时直接插在head后面
if (temp.next == null) {
temp.next = node;
return true;
}
//第二次插入 判断一下这个节点的id和第一个元素的id大小,保证此时链表中有两个节点
if (temp.next.next == null) {
if (temp.next.getData().getId() > id) {
node.next = temp.next;
temp.next = node;
return true;
} else {
temp.next.next = node;
return true;
}
}
//经过上面两步 说明此时链表中已经有两个节点,然后后面来的节点要插入到这个链表中
//所以说有三种情况
/*
* 1、当要插入的节点的id比第一个节点的id小,那么直接插到第一个节点的前面即可
* 2、遍历节点判断当要插入的节点的id刚好在某两个节点id之间,那么就插入到这两个节点中
* 3、当要插入的节点比最后一个节点的id都大,那么直接找到最后一个节点,插到它的后面即可
* */
while (temp.next != null) {
//id比第一个元素小 直接插入到最前面
if (temp.next.getData().getId() > id) {
node.next = temp.next;
temp.next = node;
return true;
} else if (temp.next.getData().getId() < id && temp.next.next.getData().getId() > id) {
temp = temp.next;
node.next = temp.next;
temp.next = node;
return true;
} else if (getLastNode().getData().getId() < id) {
HeroNode lastNode = getLastNode();
lastNode.next = node;
return true;
}
temp = temp.next;
}
return false;
}
3.2.7删除节点
思路分析
要删除某一节点B,只需要找到B的前一个节点A,然后直接将A指向B的后一个节点即可,B没有引用指向就会被GC掉
代码实现
public boolean delNode(Integer id) {
HeroNode temp = head;
while (temp.next != null){
//找到这个节点的上一个节点 然后直接将这个节点的上一个节点指向这个节点的下一个节点
if (temp.next.getData().getId().equals(id)){
temp.next = temp.next.next;
return true;
}
temp = temp.next;
}
return false;
}
3.2.8修改节点的data
思路分析
修改的话不需要改变节点的指向关系,只需要遍历链表,然后根据某一条件找出要修改一个节点,获取data域然后修改即可
代码实现
public boolean changeNode(Integer id, Hero hero) {
HeroNode temp = head;
//遍历链表
while (temp.next != null) {
//判断 找出符合条件的节点 修改data即可
if (temp.next.getData().getId().equals(id)) {
temp.next.setData(hero);
return true;
}
temp = temp.next;
}
return false;
}
3.2.9单链表经典题
- 链表反转
(双指针)
- 删除链表中重复的节点(
创建虚拟头结点
) - 合并两个有序链表
- 逆序打印单链表
(栈实现最好)
- 。。。
3.3双向链表
3.3.1定义
双向链表比单向链表多了一个指向前一个节点的指针。
public class DoubleNode {
int data;
DoubleNode next;
//指向前一个节点
DoubleNode pre;
public DoubleNode(int data) {
this.data = data;
}
}
3.3.2遍历
public void showData() {
DoubleNode temp = head.next;
while (temp != null) {
System.out.println(temp.data);
temp = temp.next;
}
}
3.3.3添加节点
先遍历找到最后一个节点,然后将最后一个节点的next指向新节点,新节点的pre指向最后一个节点
public boolean addNode(DoubleNode newNode) {
DoubleNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
//此时temp就是最后一个节点
temp.next = newNode;
newNode.pre = temp;
return true;
}
3.3.4删除节点
凡是删除节点,可能会删除第一个数据节点(非头结点),如果没有头结点就要定义一个头结点指向第一个节点,因为这里我们定义了头结点,就不需要定义头结点了。
删除节点的思路:
遍历链表,指针指向待删除节点(双向链表),然后将前一个节点的next指向待删除节点的next,将待删除节点的后一个节点的pre指向待删除节点的上一个节点,如果要删除最后一个节点,直接将它的上一个节点指向null即可
public boolean delNode(int val) {
DoubleNode temp = head;
while (temp.next != null) {
if (temp.data == val) {
//删除最后一个节点 将它的前一个节点的next置为null即可
if (temp.next == null) {
temp.pre.next = null;
return true;
} else {
temp.next.pre = temp.pre;
temp.pre.next = temp.next;
return true;
}
}
temp = temp.next;
}
return false;
}
3.3.5在某一节点前插入节点
public boolean insertBeforeNode(int targetVal,int newVal){
DoubleNode temp = head;
while (temp!=null){
if (temp.data == targetVal){
DoubleNode newNode = new DoubleNode(newVal);
//新节点next指向目标节点
newNode.next = temp;
//新节点pre指向目标节点的前一个节点
newNode.pre = temp.pre;
//目标节点的前一个节点next指向新节点
temp.pre.next = newNode;
//目标节点pre指向新节点
temp.pre = newNode;
return true;
}
temp = temp.next;
}
return false;
}
3.4单向环形链表
3.4.1概述
跟单链表基本相同,只是最后一个节点的next不指向null,指向的是第一个节点
,这样所有的节点形成一个环
3.4.2约瑟夫问题
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
可以使用单向循环链表,将每个人看作是一个节点,出列的人就删除这个节点,当最后一个节点也被删除时就结束
3.4.3编码实现
3.4.3.1定义
定义一个不带头结点的链表
节点类,跟单链表相同
public class ListNode {
//data域
public Integer val;
//next域
public ListNode next;
ListNode(Integer val) {
this.val = val;
}
}
链表类
public class SingleCycleLinkedList {
//第一个节点 先让他的data等于-1(这里随便)
public ListNode head = new ListNode(-1);
//构成环 将它的next指向自己
{
head.next = head;
}
}
3.4.3.2获取最后一个节点
根据最后一个元素指向头节点,进行循环找到最后一个节点
public ListNode getLastNode() {
ListNode htemp = head;
while (htemp.next != head) {
htemp = htemp.next;
}
return htemp;
}
3.4.3.3添加元素
先获取最后一个节点,然后将最后一个节点的next指向新节点,新节点的next指向头结点
public boolean addNode(ListNode newNode) {
ListNode lastNode = getLastNode();
lastNode.next = newNode;
newNode.next = head;
return true;
}
3.4.3.4根据data获取某个节点
public ListNode getNode(Integer data) {
//如果是头结点或者是只有一个节点 直接返回头结点
if (head.val.equals(data) || head = head.next) {
return head;
}
//指向head的next
ListNode htemp = head.next;
while (true) {
//指针移动
htemp = htemp.next;
//判断值相等 直接将这个节点返回
if (htemp.val.equals(data)) {
return htemp;
}
//如果此时htemp指向了最后一个节点 结束循环
if (htemp.next == head) {
break;
}
}
//没找到 返回null
return null;
}
3.4.3.5遍历节点
public void showNode() {
ListNode htemp = head;
while (true) {
System.out.println(htemp.val);
htemp = htemp.next;
//htemp走到了head 结束
if (htemp == head) {
break;
}
}
}
3.4.3.6约瑟夫问题解决
将约瑟夫问题转换成代码问题,就是现在有一个环形链表,循环删除从第n个节点开始的第count-1个节点,最终会删除所有的节点,将这些出队的节点按照顺序打印出来。
思路
先找到第n个节点,然后找到它的第count-2个节点==(即待删除节点的前一个节点)==,然后这个节点的下一个节点删除,然后指针指向删除节点的下一个节点,循环进行,知道剩下一个节点,即它的next等于它本身,然后将这个节点出队,终止循环
public void josephu(int begin, int count) {
//先找到第begin个节点
ListNode target = getNoNode(begin);
//循环
while (true) {
//找到待删除节点的上一个节点 指针指向它
for (int i = 0; i < count - 2; i++) {
target = target.next;
}
//然后将待删除的节点出队
System.out.printf("出队的数据是%d\n", target.next.val);
//删除节点
target.next = target.next.next;
//指向删除节点的下一个节点
target = target.next;
//判断只剩下一个节点 节点删除后终止
if (target.next == target) {
System.out.printf("出队的数据是%d\n", target.val);
break;
}
}
}
//获取第n个节点
public ListNode getNoNode(int n) {
if (n == 1) {
return head;
}
ListNode htemp = head;
n = n - 1;
while (n > 0) {
htemp = htemp.next;
n--;
}
return htemp;
}
3.4.3.7测试
@Before
public void addData(){
linkedList = new SingleCycleLinkedList();
//将第一个节点的数据置为1
linkedList.head.val = 1;
//依次添加数据
linkedList.addNode(new ListNode(2));
。。。。
}
@Test
public void test(){
//从第一个节点开始,循环删除第三个节点,打印出出队顺序
linkedList.josephu(1, 3);
}
测试成功!!!
4、链表模拟队列
链表模拟队列,使用单向链表,入队时在尾部添加节点,出队时返回并删除第一个节点(非头结点)
,做到了先进先出以及重复利用,这里可以限制节点个数也可以不限制,下面演示一种限制节点数的队列
节点
public class QueueNode {
QueueNode next;
int data;
QueueNode(int data) {
this.data = data;
}
}
队列
public class LinkedQueue {
//头节点
QueueNode head = new QueueNode(-1);
//最大节点数(不包含头结点)
int maxSize;
//记录节点数(不包含头结点)
int size = 0;
//初始化队列 规定最大节点数
LinkedQueue(int maxSize) {
this.maxSize = maxSize;
}
//返回最后一个节点(添加节点时方便)
private QueueNode getLastNode() {
if (size == 0) {
throw new RuntimeException("队空");
}
QueueNode temp = head.next;
while (temp.next != null) {
temp = temp.next;
}
return temp;
}
//添加 入队 添加到最后 size++
public boolean push(QueueNode newNode) {
if (size == maxSize) {
throw new RuntimeException("队满");
}
if (size == 0) {
head.next = newNode;
size++;
return true;
}
QueueNode lastNode = getLastNode();
lastNode.next = newNode;
size++;
return true;
}
//出队 删除第一个结点 size--
public QueueNode pop() {
if (size == 0) {
throw new RuntimeException("队空");
}
QueueNode res = head.next;
head.next = head.next.next;
size--;
return res;
}
//遍历节点
public void showData() {
if (size == 0) {
throw new RuntimeException("队空");
}
QueueNode temp = head.next;
while (temp != null) {
System.out.println(temp.data);
temp = temp.next;
}
}
}
5、栈(Stack)
5.1栈概述
- 栈是
先进后出
的一种有序列表 (队列是先进先出) - 栈中的元素的插入和删除只能在同一端进行,即
栈顶(Top)
,另一端不进行操作,成为栈底(Bottom)
- 栈的操作主要是
入栈(push)
和出栈(pop)
入栈示意图
出栈示意图
5.2栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图的深度优先(depth一first)搜索法。
5.3数组模拟栈
数组模拟栈总体比较简单,只需要使用一个指针控制入栈的元素索引即可
public class ArrayStack {
//数组 存储数据
private int[] stack;
//初始指向-1
private int top = -1;
private int maxSize;
ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new int[maxSize];
}
//判断 当top指向栈顶时栈满
public boolean isFull() {
return top == maxSize - 1;
}
//判断栈空
public boolean isEmpty() {
return top == -1;
}
//入栈
public boolean push(int value) {
if (isFull()) {
System.out.println("栈满");
return false;
}
//指针上移 将数据插入
top++;
stack[top] = value;
return true;
}
//出栈
public int pop(){
if (isEmpty()){
throw new RuntimeException("栈空");
}
//取出元素 指针下移
int data = stack[top];
top--;
return data;
}
//遍历(从上到下)
public void showData(){
if (isEmpty()){
throw new RuntimeException("栈空");
}
for (int i = top; i >= 0; i--) {
System.out.println(stack[i]);
}
}
}
5.4链表模拟栈
5.4.1无限容版
链表模拟栈也比较简单,设置一个头节点,然后使用头插法
,后来的元素(入栈时)插入到头结点的next,出栈只需要将第一个元素删除(非头结点)即可。
节点代码
public class StackNode {
StackNode next;
int data;
StackNode(int data) {
this.data = data;
}
}
栈代码
因为是不限制容量,所以只需要定义一个头结点即可。
public class LinkedStack {
//头结点
private StackNode head = new StackNode(-1);
LinkedStack() {
}
//入栈 头插法
public boolean push(StackNode newNode) {
//第一次插入 直接插入到head节点后
if (head.next == null) {
head.next = newNode;
return true;
}
newNode.next = head.next;
head.next = newNode;
return true;
}
//出栈 先将第一个节点保存起来,然后删除 最后返回即可
public StackNode pop() {
if (head.next == null) {
throw new RuntimeException("栈空");
}
StackNode res = head.next;
head.next = head.next.next;
return res;
}
//遍历
public void showData() {
StackNode temp = head.next;
while (temp != null) {
System.out.println(temp.data);
temp = temp.next;
}
}
}
5.4.2限容版
限容的话只需要定义一个maxSize表示链表中最多的节点个数(不包括头结点),然后定义一个size记录栈中的节点数,与maxSize比较,当size=maxSize时就不能添加节点了
public class LinkedStack {
private StackNode head = new StackNode(-1);
//每次元素入栈时记录元素个数
private int size = 0;
//最大的节点数 构造栈时指定
private int maxSize;
LinkedStack(int maxSize) {
this.maxSize = maxSize;
}
//入栈 头插法
public boolean push(StackNode newNode) {
//判断当size == maxSize时说明节点数已达上限 无法添加
if (size == maxSize) {
throw new RuntimeException("栈满");
}
if (head.next == null) {
head.next = newNode;
//添加后size++
size++;
return true;
}
newNode.next = head.next;
head.next = newNode;
//添加后size++
size++;
return true;
}
//出栈 返回并删除节点
public StackNode pop() {
if (head.next == null) {
throw new RuntimeException("栈空");
}
StackNode res = head.next;
head.next = head.next.next;
//出栈 size--
size--;
return res;
}
}
5.5栈实现计算器(不带括号版)
大体思路
使用两个栈,一个存数字 一个存运算符
比如一个32-2*3-3*2+1
使用双指针进行扫描,因为可能有多位数,所以当后指扫描到运算符时停下,取出运算符和数字,
当两个栈都为空时,直接入栈
- 都不为空时,数字直接入栈 运算符入栈时要跟栈顶运算符进行比较 当比栈顶运算符优先级高时,直接入栈
- 当优先级比栈顶运算符低或者等于时 从数字栈中弹出两个数并弹出运算符栈的栈顶元素进行计算 ,
然后判断这时的栈顶运算符是否是符号,如果是符号,弹出并入栈一个正号,并且将计算结果变为负数放到栈中
最后将本次扫描到的运算符入栈
public int Cal(String str){
ArrayStackCal numStrack = new ArrayStackCal(10);
ArrayStackCal opeStrack = new ArrayStackCal(10);
int start = 0;
int end = 0;
while (true) {
while (end < str.length() && (str.charAt(end) != '+' && str.charAt(end) != '-' && str.charAt(end) != '*' && str.charAt(end) != '/')) {
end++;
}
//最后一个数据
if (end == str.length()) {
// 将最后一个数据入栈 然后
numStrack.push(Integer.parseInt(str.substring(start, end)));
//循环符号栈中的符号 进行操作
int count = opeStrack.top;
for (int i = 0; i <= count; i++) {
int temp = ArrayStackCal.cal(numStrack.pop(), numStrack.pop(), (char) opeStrack.pop());
numStrack.push(temp);
}
break;
}
//第一次直接放 操作数栈和符号栈都为空 直接放
if (start == 0) {
numStrack.push(Integer.parseInt(str.substring(start, end)));
opeStrack.push((str.charAt(end)));
end++;
start = end;
continue;
}
//数字直接入栈
numStrack.push(Integer.parseInt(str.substring(start, end)));
//符号判断 当前运算符优先级大于栈中的运算符 运算符直接入栈
if (ArrayStackCal.priority((char) opeStrack.peek()) < ArrayStackCal.priority(str.charAt(end))) {
opeStrack.push((str.charAt(end)));
// 当前运算符优先级小于或等于栈中的运算符 从数字栈中弹出两个值并从符号栈中弹出一个运算符进行计算 然后结果入栈 最后将本次的运算符入符号栈
} else {
int res = ArrayStackCal.cal(numStrack.pop(), numStrack.pop(), (char) opeStrack.pop());
//判断如果前面是符号 将符号变为正号 然后将结果加上符号 否则结果会错误
if (opeStrack.peek()=='-'){
opeStrack.pop();
opeStrack.push('+');
numStrack.push(-res);
opeStrack.push(str.charAt(end));
}else {
numStrack.push(res);
opeStrack.push(str.charAt(end));
}
}
end++;
start = end;
}
return numStrack.pop();
}