数据结构_线性结构

数据结构与算法

代码-码云地址

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();
    }

5.6队列和栈互相模拟

两个栈模拟队列

两个队列模拟栈

猜你喜欢

转载自blog.csdn.net/qq_46312987/article/details/119316922