1.2.java数据结构与算法——第三章,第四章(稀疏数组和队列;链表)--师承尚硅谷,韩顺平

说明:

  • 哥么看着B站尚硅谷,韩顺平讲解的。(不能理解韩顺平和尚硅谷的关系,也不关心)。同样也是相应的根据他们的笔记做的适用于自己的笔记
  • 哔哩哔哩大学毕业:p

线性结构和非线性结构

  1. 线性结构:
    1)线性结构:数据元素一对一的线性关系
    2)两种存储结构:顺序存储(顺序表)和链式存储(链表)
    3)常见:数组、队列、链表和栈

  2. 非线性结构:
    包括二维数组,多维数组,广义表,树结构,图结构…


第一章、稀疏数组和队列

1. 稀疏数组

简单图解:

在这里插入图片描述

二维数组 转 稀疏数组的思路:

  1. 遍历 原始的二维数组 , 得到有效数据的个数sum
  2. 根据sum,创建 稀疏数组sparseArr int [sum + 1 ] [3]
  3. 将 二维数组的有效数据 ,存入 稀疏数组中

稀疏数组 还原 二维数组

  1. 先读取 稀疏数组 的第一行,根据第一行的数据,创建原始的 二维数组 ,比如上面的 chessArr2 = int [11] [11]
  2. 读取稀疏数组的后面几行, 并赋值给 还原后的二维数组 。

上代码

package com.wts.sparsearrary;

public class SparseArrary {
    public static void main(String[] args) {
        //创建一个原始的二维数组 11*11
        //0:表示没有棋子, 1表示黑子,  2表示篮子
        int chessArr[][] = new int[11][11];
        chessArr[1][2] = 1;
        chessArr[2][3] = 2;
        //输出原始的二维数组
        System.out.println("二维数组为-----");
        for (int[] row : chessArr) {
            for (int data : row) {
                System.out.print(data + "\t");
            }
            System.out.println();
        }


        //二维数组 --->  稀疏数组
        int sum = 0;
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++) {
                if (chessArr[i][j] != 0) {
                    sum++;
                }
            }
        }

        //创建稀疏数组
        int sparseArr[][] = new int[sum + 1][3];
        sparseArr[0][0] = 11;
        sparseArr[0][1] = 11;
        sparseArr[0][2] = sum;
        //将非0数字存入稀疏数组
        int count = 0;
        for (int i = 0; i < 11; i++) {
            for (int j = 0; j < 11; j++) {
                if (chessArr[i][j] != 0) {
                    count++;
                    sparseArr[count][0] = i;
                    sparseArr[count][1] = j;
                    sparseArr[count][2] = chessArr[i][j];
                }
            }
        }
        //输出稀疏数组
        System.out.println("二维数组->稀疏数组如下------");
        for (int i = 0; i < sparseArr.length; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.print(sparseArr[i][j] + "\t");
            }
            System.out.println();
        }


        /*---稀疏数组 恢复成 二维数组---*/
        /*
         * 1、
         * 2、
         * */

        int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
        for (int i = 1; i < sparseArr.length; i++) {
            chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
        }
        //输出二维数组
        System.out.println("稀疏数组->二维数组------");
        for (int[] row : chessArr2) {
            for (int data : row) {
                System.out.print(data + "\t");
            }
            System.out.println();
        }

    }
}

效果如下:

在这里插入图片描述

2. 数组模拟队列思路

1.简述:

  1. 队列是一个有序列表,可以用数组或者是链表来实现
  2. 先入先出

图解:

在这里插入图片描述
实现思路:

  • maxSize是该队列的最大容量
  • 两个变量,frontrear分别记录队列的前后端下标;front会随着数据的输出改变,rear会随着数据的输入而改变
  • 数据存入队列两个步骤(思路):
    1.尾指针后移,rear+1,当front == rear为空。
    2.当尾指针rear小于队列的最大下标maxSize - 1,将数据存入rear所指的数组元素中,否则不存入;rear == maxSize - 1 //队列满
  • 牢记:front指向的是队头数据的前一个位置,rear则是直接指向的是队尾数据

上代码:(数组简单实现队列)

package com.wts.sparsearrary;

import java.util.Scanner;

public class ArrayQueueDemo {
    public static void main(String[] args) {

        ArrayQueue arrayQueue = new ArrayQueue(3);
        boolean flag = true;
        char key = ' ';
        Scanner sc = new Scanner(System.in);


        while (flag) {
            System.out.println("------菜单提示------");
            System.out.println("s(show) : 显示队列");
            System.out.println("e(exit) : 退出程序");
            System.out.println("a(add)  : 添加数据到队列");
            System.out.println("g(get)  : 从队列取出数据");
            System.out.println("h(head) : 查看队头");

            key = sc.next().charAt(0);
            //这里为什么只能用sc.next()呢??
            /*
            * sc.next()和sc.nextLine()有什么区别呢
            这里用nextLine()就会报错哎
            */
            switch (key) {
                case 's': {
                    arrayQueue.showQueue();
                    break;
                }
                case 'e': {
                    flag = false;
                    break;
                }
                case 'a': {
                    System.out.print("--输入要添加的数据:");
                    int n = sc.nextInt();
                    arrayQueue.addQueue(n);
                    break;

                }
                case 'g': {
                    try {

                        int result = arrayQueue.getQueue();
                        System.out.println("取出的数据为:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                }

                case 'h': {
                    try {
                        arrayQueue.headQueue();
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }

                    break;
                }
            }
        }
    }
}

//使用数组模拟队列 --编写ArrayQueue类
class ArrayQueue {
    private int maxSize; // 数组的最大容量,队列的容量
    private int front; // 队头
    private int rear; // 队尾
    private int[] arr; // 数组,用于存放数据,模拟队列

    //队列构造器-数组
    public ArrayQueue(int maxSize) {
        this.maxSize = maxSize;
        this.front = -1;
        this.rear = -1;
        arr = new int[maxSize];
    }

    //判断队列是否满
    public boolean isFull() {
        if (rear == maxSize - 1) {
            return true;
        }
        return false;
    }

    //判断队列是否为空
    public boolean isEmpty() {
        return rear == front;
    }

    //添加数据 入队列
    public void addQueue(int n) {
        //判断满?
        if (isFull()) {
            System.out.println("队列已满!无法添加!");
            return;
        }
        rear++;
        arr[rear] = n;
    }

    //获取队列的数据,出队列
    public int getQueue() {
        //判断是否空?
        if (isEmpty()) {
            throw new RuntimeException("队列空!请先添加数据!");
        }
        front++;
        return arr[front];
    }

    //遍历
    public void showQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列空!请先添加数据!");
        }
        for (int data : arr) {
            System.out.println(data);
        }
    }

    //显示队头
    public void headQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列空!请先添加数据!");
        }
        System.out.println(arr[front + 1]);
    }
}

小结:

  • 数组只能使用一次,没有达到复用的效果
  • 遇到的问题在代码的注释上标注了
    • 优化思路:改进成一个环形的队列 取模:%

环形队列优化!
环形

3. 数组模拟环形队列思路

环形数组优化,(取模的方式实现),环形队列

简述与图解:

  • 需要明白的是:front和rear都是从0开始
  • front指向队头数据的当前位置, 也就是说,arr[front]表示队头数据
  • rear指向队尾数据的下一个位置,因为希望空出一个空间作为约定
  • 尾索引的下一个为头索引表示队列已经满了:(rear+1) % maxsize == front 【满】
  • 队列空:rear == front
  • 也就是说,最大存储maxsize实际上最大能存储的值是maxsize-1

(((参考博主文章:数组实现环形队列(图解))))

在这里插入图片描述

上代码:

package com.wts.sparsearrary.CircleQueue;


import java.util.Scanner;

public class CircleArrayQueueDemo {
    public static void main(String[] args) {

        CircleArrayQueue circleArrayQueue = new CircleArrayQueue(4);//说明是 4,实际有效最大长度为3
        boolean flag = true;
        char key = ' ';
        Scanner sc = new Scanner(System.in);


        while (flag) {
            System.out.println("------菜单提示------");
            System.out.println("s(show) : 显示队列");
            System.out.println("e(exit) : 退出程序");
            System.out.println("a(add)  : 添加数据到队列");
            System.out.println("g(get)  : 从队列取出数据");
            System.out.println("h(head) : 查看队头");

            key = sc.next().charAt(0);
            //这里为什么只能用sc.next()呢??
            /*
            * sc.next()和sc.nextLine()有什么区别呢
            这里用nextLine()就会报错哎
            */
            switch (key) {
                case 's': {
                    try {
                        circleArrayQueue.showCircleQueue();
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                }
                case 'e': {
                    flag = false;
                    break;
                }
                case 'a': {
                    try {
                        System.out.print("--输入要添加的数据:");
                        int n = sc.nextInt();
                        circleArrayQueue.addCircleQueue(n);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                }
                case 'g': {
                    try {
                        int result = circleArrayQueue.getCircleQueue();
                        System.out.println("取出的数据为:" + result);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                }

                case 'h': {
                    try {
                        circleArrayQueue.headOfCircleQueue();
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }

                    break;
                }
            }
        }
    }

}

class CircleArrayQueue {
    private int front;//指向队头数据
    private int rear;// 指向队尾数据的后一位
    private int maxsize;
    private int[] arr;

    CircleArrayQueue(int n) {
        this.maxsize = n;
        front = 0;
        rear = 0;
        arr = new int[maxsize];
    }

    public boolean isEmpty() {
        if (rear == front) {
            return true;
        }
        return false;
    }

    public boolean isFull() {
        if ((rear + 1) % maxsize == front) {
            return true;
        }
        return false;
    }

    //add data into circlequeue
    public void addCircleQueue(int n) {
        if (isFull()) {
            throw new RuntimeException("队列已满!");
        }
        arr[rear] = n;
        rear = (rear + 1) % maxsize;
    }

    //get data from cq
    public int getCircleQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        int value = arr[front];
        front = (front + 1) % maxsize;
        return value;
    }

    //size of cq
    public int Size() {
        return (rear - front + maxsize) % maxsize;
    }

    //head of cq
    public void headOfCircleQueue() {
        System.out.println("队头数据为:" + arr[front]);
    }

    //iterate through cq
    public void showCircleQueue() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        //这个遍历没想到 ,还挺细节!
        for (int i = front; i < front + Size(); i++) {
            System.out.println(arr[i % maxsize]);
        }
    }
}

第二章、链表

1. 链表简介

存储图如下:
在这里插入图片描述

小结上图:

  • 节点方式存储,链式存储
  • 每个节点包含data域next域
  • 不一定连续存储
  • 链表分为带头节点的链表没有头节点的链表

在这里插入图片描述

2. 单链表操作

1. 单链表初始化:

总共三部分实现单链表:Demo类、SingleList类(管理单链表)、节点类

上代码:
1 ) 结点类:主要是data部分(这里的data可以包含很多信息)和next部分

/*结点*/
class ListNode {
    public int no;
    public String data;
    public ListNode next;

    public ListNode(int no, String data) {
        this.no = no;
        this.data = data;
    }

    @Override
    public String toString() {
        return "ListNode{" +
                "no=" + no +
                ", data='" + data + '\'' +
                '}';
    }
}

2 ) 单链表管理:包含头结点的初始化、一些单链表的增删改查相关方法等

/*单链表管理*/
class SingleList {

    //初始化头节点,头节点不放任何数据
    ListNode head = new ListNode(0, "");
    public void addList(){...}
    public void showList(){...}
    ......
}

3 )Demo:main函数

public class LinkTestDemo {
    public static void main(String[] args) {

        //初始化一些结点
        ListNode node1 = new ListNode(01, "01");
        ListNode node2 = new ListNode(02, "02");
        ListNode node3 = new ListNode(03, "03");

        SingleList singleList = new SingleList();

        /*尾添加 测试*/
        singleList.addNode(node1);
        singleList.addNode(node2);
        singleList.addNode(node3);

        /*遍历 */
        singleList.showList();
    }
}

2. 添加

1)尾添加:
上代码:

//增加数据,尾添加
public void addNode(ListNode node) {
    ListNode temp = head;

    //temp 指向 尾结点
    while (true) {//遍历到尾结点
        if (temp.next == null) {
            break;
        }
        temp = temp.next;
    }
    temp.next = node;
}

2) 插入添加(按照编号no插入添加)
理解图:
在这里插入图片描述

上代码:

    //插入节点--按照编号no加入结点
    //思路:
    // 1.找到要插入位置的前一个结点,temp指向它;
    //2.如何找到呢?遍历!--temp.next.no > node. no
    public void addByOrder(ListNode node) {
        ListNode temp = head;//插入结点的用到的temp有讲究的:temp指向插入位置的前一个结点
        //遍历-定位到对应的编号no的位置
        while (true) {
            if (temp.next == null) {//遍历到末尾了
                break;
            }
            if (temp.next.no == node.no) {
                //遇到编号no相同的,不能添加
                System.out.println("已经存在编号" + node.no + "不能添加!");
                return;
            }
            if (temp.next.no > node.no) {//找到了位置了!
                break;
            }
            temp = temp.next;
        }

        node.next = temp.next;
        temp.next = node;

    }
}

3. 遍历:先判断链表是否为空,从第一个有效节点开始遍历

上代码:


//遍历链表:
public void showList() {
    //判断表格空 可以直接写在这里,不用循环的
    if (head == null) {
        System.out.println("链表为空!");
        return;
    }
    ListNode temp = head.next;//头结点没有数据,这里表示第一个有效结点
    while (true) {
        if (temp == null) {
            return;
        }
        System.out.println(temp);
        temp = temp.next;
    }
}
// ---编程问题总结
// --- 1.指针temp创建的时候,我把它写在了while循环里面,导致每次都读取第一个结点的数据,无限死循环。
// --- 2.判断是否到了最后一个节点的 时候,我写的temp.next==null,导致最后一个结点输出不来。

4. 修改

根据编号no的值,修改结点数据
思路:temp.no == newnode.no定位
上代码:

    //修改结点信息-- 根据no更改结点的数据
    //1.temp.no == newnode.no
    public void updateNode(ListNode newnode) {
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }

        ListNode temp = head;
        boolean flag = false;
        //定位
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.no == newnode.no) {
                flag = true;//定位成功
                break;
            }
            temp = temp.next;
        }
        if (flag){
            temp.data = newnode.data;
        }else {
            System.out.println("未能找到匹配的结点");
        }
    }

5.删除

思路:temp到底是指向前一个位置还是指向当前需要操作的位置,自己好好理解、甄别。

上代码:

	public void deleteNode(ListNode node) {
       if (head.next == null) {
           System.out.println("链表为空!");
           return;
       }

       ListNode temp = head;
       boolean flag = false;
       while (true) {
           if (temp == null) {
               break;
           }
           if (temp.next.no == node.no) {
               flag = true;
               break;
           }
           temp = temp.next;
       }
       if(flag){
           temp.next = temp.next.next;
       }else {
           System.out.println("未能找到编号" + node.no + "的数据");
       }
   }

效果:

在这里插入图片描述

6. 单链表长度(不计头结点)

思路:遍历,计数
上代码:

    //获取单链表的长度--不统计头结点
    public int getLength() {
        int count = 0;
        if (head.next != null) {
            //如果链表非空
            ListNode temp = head.next;
            while (temp != null) {
                count++;
                temp = temp.next;
            }
        }
        return count;
    }

效果:
在这里插入图片描述

7. 获取倒数第k个结点

思路:从第一个有效结点开始遍历,倒数第index个,遍历length - index
例如,长度4,倒数第k=2个,从第一个有效节点开始往后遍历4-2=2次 。
上代码:

    //获取倒数第k个结点--
    //思路:index为倒数第几个,从第一个有效结点开始,往后遍历(length - index)次  
    public ListNode getLastIndexNode(int index) {
        int length = getLength();
        if (length == 0) {
            return null;
        }
        if (index <= 0 && index > length) {
            return null;
        }
        ListNode temp = head.next;
        for (int i = 0; i < length - index; i++) {
            temp = temp.next;
        }
        return temp;
    }

效果:
在这里插入图片描述

8. 单链表反转

思路:

  • 首先要理解一个点:SingLink类(管理单链表类),刚开始初始化的头结点head,表示只认head为头的结点;也就是说后续方法里面新创建的任何新的头结点(比如rehead),进行相关的更改链表操作后,最后还是要基于head为头的结点 即,重新保证头结点是head
  • 所以思路就很清晰:
    1 )temp从第一个有效结点开始,同时记录temp的下一个结点tempnext
    2 )新的头结点rehead,用头插法,把temp和新的rehead头结点相连
    3 ) 最后更换头结点为head,即,让head指向新链表的第一个有效节点

上代码:

    public void reserveList() {
        //链表为空,或只有1个结点
        if (head.next == null && head.next.next == null) {
            System.out.println("链表长度小于2,无法反转!");
            return;
        }

        //新的头结点,-- 我的个人理解是:这是开启了2号链表方便操作,但是最后把头结点换掉!
        ListNode rehead = new ListNode(0, "");

        ListNode temp = head.next;//从第一个有效结点开始
        while (temp != null) {
            ListNode tempnext = temp.next;//--很重要!

            temp.next = rehead.next;
            rehead.next = temp;
            temp = tempnext;  //--编程问题总结:这里要好好理解tempnext的作用!我总是写成"temp = temp.next",报错,因为上面已经把temp.next改变了,(也就是说改变了temp后面的连接的了)
            //--需要一个tempnext在次之前记录 源列表temp的后一个结点的信息!
        }
        //最后一定要换头结点
        head.next = rehead.next;
    }

反转效果:
在这里插入图片描述

9. 单链表反转打印(不改变原列表)

思路:栈,先进后出
上代码:

    //**反转打印--不改变原来列表的顺序--使用栈
    public void reversePrint() {
        if (head.next == null) {
            System.out.println("List is empty!");
            return;
        }
        //创建栈
        Stack<ListNode> stack = new Stack<>();

        ListNode temp = head.next;
        while (temp != null) {
            //入栈
            stack.push(temp);
            temp = temp.next;
        }

        //出栈
        while (stack.size() > 0) {
            ListNode pop = stack.pop();
            System.out.println(pop);
        }
    }

效果展示:
在这里插入图片描述

10. 合并两个链表(课后习题)

思路:遍历第一个链表,插入到第二个链表中
上代码:

    //合并两个有序数列
    public void combineLists(SingleList singleList1, SingleList singleList2) {
        if (singleList1.head.next == null || singleList2.head.next == null) {
            return;
        }

        ListNode temp1 = singleList1.head.next;//temp1当前结点
        ListNode temp1next = null;

        while (temp1 != null) {
            temp1next = temp1.next;//这里又用到tempnext了,为什么用?什么时候用??
            //我的理解:当问题涉及到两个链表的时候,因为temp1结点可能需要插入到一个新的位置,导致他在原来结点位置的下一个结点和新位置的结点是不一致的,
            // 所以需要提前记录temp1原来的下一个结点temp1next!!
            ListNode temp2 = singleList2.head;// temp2 要插入位置的前一个结点

            while (temp2 != null) {
                boolean flag = false;//是否在List2添加成功,
                if (temp1.no == temp2.next.no) {//结点重复
                    break;
                }
                if (temp1.no < temp2.next.no) {
                    flag = true;
                }
                if (flag) {
                    temp1.next = temp2.next;
                    temp2.next = temp1;
                    break;
                }else {
                    temp2 = temp2.next;
                }
            }
            temp1 = temp1next;
        }
    }

总结:什么时候记录tempnext?
答: 涉及到两个链表的时候,当temp结点插入到新的链表的时候,同时又要满足temp 的下一节点也要进行相关操作

效果:
在这里插入图片描述

1-10,总体代码参考

package com.wts._04LinkedList;

import java.util.Stack;

public class LinkTestDemo {
    public static void main(String[] args) {

        //初始化一些结点
        ListNode node1 = new ListNode(01, "01");
        ListNode node2 = new ListNode(02, "02");
        ListNode node4 = new ListNode(04, "04");
        ListNode node3 = new ListNode(03, "03");

        SingleList singleList = new SingleList();

        /*尾添加 测试*/
//        singleList.addNode(node1);
//        singleList.addNode(node2);
//        singleList.addNode(node4);
//        singleList.addNode(node3);
        /*遍历 */
        //singleList.showList();

        //按照编号no插入添加
        singleList.addByOrder(node4);
        singleList.addByOrder(node1);
        singleList.addByOrder(node3);
        singleList.addByOrder(node2);
        singleList.showList();

        //修改结点信息
        ListNode node_1 = new ListNode(2, "0002");
        singleList.updateNode(node_1);
        System.out.println("----修改后的结点为:");
        singleList.showList();

/*        //删除
        singleList.deleteNode(node2);
        System.out.println("删除后的链表--");
        singleList.showList();*/

        //节点个数--链表长度
        int length = singleList.getLength();
        System.out.println("链表结点个数为:" + length);
        System.out.println("-------");

        //获取倒数第k个结点
        System.out.println("倒数第2个结点为--");
        ListNode lastIndexNode = singleList.getLastIndexNode(2);
        System.out.println(lastIndexNode);

        //--链表反转
        System.out.println("--未反转前:");
        singleList.showList();
        System.out.println("--反转后:");
        singleList.reserveList();
        singleList.showList();

        //--链表反转输出--栈
        System.out.println("反链表-反转打印--");
        singleList.reversePrint();
        System.out.println("--不改变链表");
        singleList.showList();

        //--两个有序合并成一个有序列表
        SingleList singleList1 = new SingleList();
        SingleList singleList2 = new SingleList();
        ListNode node01 = new ListNode(01, "001");
        ListNode node03 = new ListNode(03, "003");
        ListNode node05 = new ListNode(05, "005");
        ListNode node06_ = new ListNode(06,"006");
        ListNode node02 = new ListNode(02, "002");
        ListNode node04 = new ListNode(04, "004");
        ListNode node06 = new ListNode(06, "006");
        singleList1.addNode(node01);
        singleList1.addNode(node03);
        singleList1.addNode(node05);
        singleList1.addNode(node06_);
        singleList2.addByOrder(node06);
        singleList2.addByOrder(node04);
        singleList2.addByOrder(node02);

        System.out.println();
        System.out.println("链表1--");
        singleList1.showList();
        System.out.println("链表2--");
        singleList2.showList();

        //合并
        System.out.println("--合并后");
        singleList2.combineLists(singleList1, singleList2);
        singleList2.showList();

    }

}

/*单链表管理*/
class SingleList {

    //初始化头节点,头节点不放任何数据
    ListNode head = new ListNode(0, "");

    //增加数据,尾添加
    public void addNode(ListNode node) {
        ListNode temp = head;

        //temp 指向 尾结点
        while (true) {//遍历到尾结点
            if (temp.next == null) {
                break;
            }
            temp = temp.next;
        }
        temp.next = node;
    }

    //遍历链表:
    // ---编程问题总结
    // --- 1.指针temp创建的时候,我把它写在了while循环里面,导致每次都读取第一个结点的数据,无限死循环。
    // --- 2.判断是否到了最后一个节点的 时候,我写的temp.next==null,导致最后一个结点输出不来。
    public void showList() {
        //判断表格空 可以直接写在这里,不用循环的
        if (head == null) {
            System.out.println("链表为空!");
            return;
        }
        ListNode temp = head.next;//头结点没有数据,这里表示第一个有效结点
        while (true) {
            if (temp == null) {
                return;
            }
            System.out.println(temp);
            temp = temp.next;
        }
    }

    //插入节点--按照编号no加入结点
    //思路:
    // 1.找到要插入位置的前一个结点,temp指向它;
    //2.如何找到呢?遍历!--temp.next.no > node. no
    public void addByOrder(ListNode node) {
        ListNode temp = head;//插入结点的用到的temp有讲究的:temp指向插入位置的前一个结点
        //遍历-定位到对应的编号no的位置
        while (true) {
            if (temp.next == null) {//遍历到末尾了
                break;
            }
            if (temp.next.no == node.no) {
                //遇到编号no相同的,不能添加
                System.out.println("已经存在编号" + node.no + "不能添加!");
                return;
            }
            if (temp.next.no > node.no) {//找到了位置了!
                break;
            }
            temp = temp.next;
        }

        node.next = temp.next;
        temp.next = node;

    }


    //修改结点信息-- 根据no更改结点的数据
    //1.temp.no == newnode.no
    public void updateNode(ListNode newnode) {
        if (head.next == null) {
            System.out.println("链表为空");
            return;
        }

        ListNode temp = head;
        boolean flag = false;
        //定位
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.no == newnode.no) {
                flag = true;//定位成功
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            temp.data = newnode.data;
        } else {
            System.out.println("未能找到匹配的结点");
        }
    }

    public void deleteNode(ListNode node) {
        if (head.next == null) {
            System.out.println("链表为空!");
            return;
        }

        ListNode temp = head;
        boolean flag = false;
        while (true) {
            if (temp == null) {
                break;
            }
            if (temp.next.no == node.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            temp.next = temp.next.next;
        } else {
            System.out.println("未能找到编号" + node.no + "的数据");
        }
    }

    //获取单链表的长度--不统计头结点
    public int getLength() {
        int count = 0;
        if (head.next != null) {
            //如果链表非空
            ListNode temp = head.next;
            while (temp != null) {
                count++;
                temp = temp.next;
            }
        }
        return count;
    }

    //获取倒数第k个结点--
    //思路:index为倒数第几个,从第一个有效结点开始,往后遍历(length - index)次  
    public ListNode getLastIndexNode(int index) {
        int length = getLength();
        if (length == 0) {
            return null;
        }
        if (index <= 0 && index > length) {
            return null;
        }
        ListNode temp = head.next;
        for (int i = 0; i < length - index; i++) {
            temp = temp.next;
        }
        return temp;
    }

    //反转链表
    //思路:!!首先要明白一个点,SingleList这个类,定义了一个head头结点,这就是表示这是一个以head为头的单链表了,
    // !!后续在这个类的方法中在创建的任何新的头结点(这里统称为rehead,),对链表做出调整修改后,仍然是基于head为头的结点,
    //!!即,需要重新连接,以head为头结点
    //1.temp从第一个有效结点开始遍历,同时统计他的下一个几点tempnext
    //2.让temp连接到新的头结点rehead(2号链表),tempnext用头插法加入新的链表
    //3. 最后让head指向新链表的第一个有效结点
    public void reserveList() {
        //链表为空,或只有1个结点
        if (head.next == null && head.next.next == null) {
            System.out.println("链表长度小于2,无法反转!");
            return;
        }

        //新的头结点,-- 我的个人理解是:这是开启了2号链表方便操作,但是最后把头结点换掉!
        ListNode rehead = new ListNode(0, "");

        ListNode temp = head.next;//从第一个有效结点开始
        while (temp != null) {
            ListNode tempnext = temp.next;//--很重要!

            temp.next = rehead.next;
            rehead.next = temp;
            temp = tempnext;  //--编程问题总结:这里要好好理解tempnext的作用!我总是写成"temp = temp.next",报错,因为上面已经把temp.next改变了,(也就是说改变了temp后面的连接的了)
            //--需要一个tempnext在次之前记录 源列表temp的后一个结点的信息!
        }
        //最后一定要换头结点
        head.next = rehead.next;
    }

    //**反转打印--不改变原来列表的顺序--使用栈
    public void reversePrint() {
        if (head.next == null) {
            System.out.println("List is empty!");
            return;
        }
        //创建栈
        Stack<ListNode> stack = new Stack<>();

        ListNode temp = head.next;
        while (temp != null) {
            //入栈
            stack.push(temp);
            temp = temp.next;
        }

        //出栈
        while (stack.size() > 0) {
            ListNode pop = stack.pop();
            System.out.println(pop);
        }
    }

    //合并两个有序数列
    public void combineLists(SingleList singleList1, SingleList singleList2) {
        if (singleList1.head.next == null || singleList2.head.next == null) {
            return;
        }

        ListNode temp1 = singleList1.head.next;//temp1当前结点
        ListNode temp1next = null;

        while (temp1 != null) {
            temp1next = temp1.next;//这里又用到tempnext了,为什么用?什么时候用??
            //我的理解:当问题涉及到两个链表的时候,因为temp1结点可能需要插入到一个新的位置,导致他在原来结点位置的下一个结点和新位置的结点是不一致的,
            // 所以需要提前记录temp1原来的下一个结点temp1next!!
            ListNode temp2 = singleList2.head;// temp2 要插入位置的前一个结点

            while (temp2 != null) {
                boolean flag = false;//是否在List2添加成功,
                if (temp1.no == temp2.next.no) {//结点重复
                    break;
                }
                if (temp1.no < temp2.next.no) {
                    flag = true;
                }
                if (flag) {
                    temp1.next = temp2.next;
                    temp2.next = temp1;
                    break;
                }else {
                    temp2 = temp2.next;
                }
            }
            temp1 = temp1next;
        }
    }
}

/*结点*/
class ListNode {
    public int no;
    public String data;
    public ListNode next;

    public ListNode(int no, String data) {
        this.no = no;
        this.data = data;
    }

    @Override
    public String toString() {
        return "ListNode{" +
                "no=" + no +
                ", data='" + data + '\'' +
                '}';
    }
}

3. 双向链表

单向链表缺点分析:
1)单向链表查找只能一个方向;双向链表还可以从后往前查找
2)单向链表不能自我删除(temp的前一个结点);双向链表可以自我删除
3)相比较单链表多了前驱pre
双链表主要可以自我操作,而单链表依赖前一个结点。
图解:
在这里插入图片描述

1. 增

1)尾插法(尾部添加)
思路:尾部添加,nextpre连接即可。

 //尾插法
    public void addNodeToTail(ListNode2 node) {
        ListNode2 temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = node;
        node.pre = temp;
    }

–有手就行

2)有序插入–根据编号no
需要思考:具体问题代码注释
上代码:

    //有序添加--根据no
    /*编程问题:
    1.第一个结点插入的时候比较特殊
    2.核心代码编写的顺序问题也要重视,先连node结点和前一个结点的关系,然后写node和后一个结点的关系
    我不知道上述是不是通法,但是要理解
     * */
    public void addByOrder(ListNode2 node) {
        ListNode2 temp = head;
        if (head.next == null) {//结点为空,特殊加入
            temp.next = node;
            node.pre = temp;
            return;
        }

        boolean flag = false;
        while (true) {
            if (temp.no == node.no) {
                System.out.println("结点已经存在!");
                break;
            }
            if (node.no < temp.no) {//找到位置
                flag = true;
                break;
            }
            if (temp.next == null) {
                break;
            }
            temp = temp.next;
        }
        //插入
        temp.pre.next = node;
        node.pre = temp.pre;

        node.next = temp;/*先后顺序?*/
        temp.pre = node;
//        错误代码展示:
//        node.next = temp;
//        temp.pre = node;**

//        temp.pre.next = node;**(错误点temp.pre已经指向了别的地方了,)
//        node.pre = temp.pre;
    }

效果:
在这里插入图片描述

思考:双向链表插入代码书写的顺序有影响的!具体思考和注意在实践中debug。

2. 删

根据no删除结点(自我删除
上代码:

    //删除结点--根据结点no的信息
    /*遇到问题:删除结点的两行核心代码,我一开始没考虑最后个结点,(最后一个结点的temp.next == null, temp.next.pre就会报错)
     * 解决方法:最后一个结点加以判断*/
    public void deleteNode(ListNode2 node) {
        if (head.next == null) {
            System.out.println("空--");
            return;
        }

        ListNode2 temp = head.next;
        boolean flag = false; //默认没找到对应的结点
        while (temp != null) {
            if (temp.no == node.no) {
                flag = true;
                break;
            }
            temp = temp.next;
        }

        if (true) {//删除节点
            temp.pre.next = temp.next;
            if (temp.next != null) {// *不是最后一个结点才可以执行下面一句话
                temp.next.pre = temp.pre;
            } else {
                System.out.println("没找对编号为" + node.no + "的结点");
                return;
            }
        }

    }

注意:考虑最后一个结点

3. 改

根据编号no–改信息**(自我修改!)**
上代码:

    //修改结点
    public void updateNode(ListNode2 node) {
        //判表空
        if (head.next == null) {
            System.out.println("双向链表为空---");
            return;
        }

        //找到需要修改的结点,根据编号no
        ListNode2 temp = head.next;
        boolean flag = false;
        while (temp != null) {
            if (temp.no == node.no) {//找到了相同的编号no
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            //找到了匹配的编号no,修改信息
            temp.id = node.id;
            temp.name = node.name;
        } else {
            System.out.println("未能找到编号为" + node.no + "的数据");
        }
    }

4. 查

遍历方式和单链表一样
上代码:

    //遍历显示--和单链表一样
    public void showList() {
        if (head.next == null) {
            System.out.println("双向链表为空--");
            return;
        }
        ListNode2 temp = head.next;
        while (temp != null) {
            System.out.println(temp);
            temp = temp.next;
        }
    }

双向链表总结

总结:双向链表相比较单链表,更加注重自我操作(即,直接对temp指向的结点修改,不依靠temp的前一个结点)。当然所谓自我操作也带来需要注意的地方,需要考虑第一个和最后一个结点,会比较特殊。

4. 单向环形链表

约瑟夫问题:
在这里插入图片描述

约瑟夫问题

单项环形链表:
尾部添加,添加进来的结点记得指向头结点实现环形
删除结点还是常规,始终找到删除节点的前一个结点

上代码:

package com.DataStructure._04LinkedList;

/**
 * 单向环形链表
 * 约瑟夫问题:
 * Josephu 问题为:设编号为1,2,...n的n个人围坐一圈,约定编号为k (1<=k<=n)的人从1开始报数,
 *         数到m的那个人出列,它的下一位接着数到m 的那个人又出列,
 *         依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
 */

public class JosepfuCircleSingleListDemo {
    public static void main(String[] args) {
        JosepfuCircleSingleList josepfuCircleSingleList = new JosepfuCircleSingleList();
        josepfuCircleSingleList.addBoy(5);
        josepfuCircleSingleList.showBoys();

        System.out.println("----出圈");
        josepfuCircleSingleList.countBoy(1,2,5);//2-4-1-5-3
    }
}

//创建一个单项环形链表
class JosepfuCircleSingleList {
    /**
     * 几个重要的点
     * first结点---第一个结点,固定不变的
     * curBoy结点---尾结点,指向新节点后定时挪动到尾部
     */

    private Boy first = null;

    public void addBoy(int num) {
        if (num < 1) {
            System.out.println("num数据不正确");
            return;
        }

        Boy curBoy = null;//临时指针
        for (int i = 1; i <= num; i++) {
            Boy boy = new Boy(i);
            if (i == 1) {
                first = boy;
                first.next = first;//闭环
                curBoy = first;//
            } else {
                curBoy.next = boy;
                boy.next = first;
                curBoy = boy;
            }
        }
    }

    //遍历
    void showBoys() {
        Boy tempBoy = first;
        while (true) {
            System.out.println(tempBoy);
            if (tempBoy.next == first) {
                break;
            }
            tempBoy = tempBoy.next;
        }
    }


    //报数和出列

    /**
     * @param startNo  开始小孩是几?
     * @param countNum 规定报数是?
     * @param nums     全部小孩的总数
     *                 first -- 出列
     *                 helper -- first前面一个(要删除结点的前一个位置)
     */
    public void countBoy(int startNo, int countNum, int nums) {
        //数据校验
        if (startNo < 1 || first == null || startNo > nums) {
            System.out.println("参数错误!");
            return;
        }
        //first定位到startNo
        for (int j = 0; j < startNo - 1; j++) {
            //first默认是第一个小孩,移动startNo-1次 到达要求的starNo的位置
            first = first.next;
        }
        //创建辅助结点helper,起始位置在first前一个
        // (first就是要删除的结点,本质就是删除结点的前一个结点),后面一个一直保证helper在first前一个位置
        Boy helper = first;
        while (true) {
            if (helper.next == first) {
                break;
            }
            helper = helper.next;
        }
        //按照规则出列,直到只有一个结点
        while (helper != first) {
            //根据报数,移动countNum-1
            for (int j = 0; j < countNum - 1; j++) {
                first = first.next;
                helper = helper.next;
            }
            //出列(删除结点)
            System.out.println(first);
            first = first.next;
            helper.next = first;
        }
        System.out.println(helper);//最后一个小孩节点
    }
}

//小孩的结点
class Boy {
    public int no;
    public Boy next;//指向下一个结点

    public Boy(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Boy getNext() {
        return next;
    }

    public void setNext(Boy next) {
        this.next = next;
    }

    @Override
    public String toString() {
        return "Boy{" +
                "no=" + no +
                '}';
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值