个人笔记记录

Java流程控制

用户交互Scanner

之前我们学的基本语法中我们并没有实现程序和人的交互,但是Java给我们提供了这样一个工具类,我们可以获取用户的输入。java.util.Scanner是Java5的新特征,我们可以通过Scanner 类来获取用户的输入
基本语法:

Scanner s = new Scanner(System.in);
……
……
s.close();

通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前我们一般需要使用hasNext()与hasNextLine()判断缓冲区内是否还有输入的数据(任意类型),若有则返回true,没有则返回false(扫描文件)或等待键盘输入(扫描控制台)

next()

package com.kimtanyo.scanner;
import java.util.Scanner;

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

        Scanner scanner = new Scanner(System.in);// 

        System.out.println("使用next方式接收:");

    //    判断用户有没有输入字符串
        if (scanner.hasNext()){  // 这里省略了==true
            // 使用next方式接收
            String str = scanner.next(); 
            System.out.println("输出的内容为"+str);
        }
        // 凡是属于IO流的类如果不关闭会一直占用资源,要养成好习惯用完就关掉
        scanner.close();
    }
}

需要注意:

  • 对于输入源为键盘System.in时,只要hasNext()或next()方法被调用,无论在什么结构里面,hasNext()或next()方法都会出现阻塞(block),即等待键盘输入才会执行后续,hasNext()一定为true;hasNextLine()和NextLine()也一样
  • 对于输入源为文件时,如果文件为空(空格、tab、回车),则hasNext()为false,否则为true;hasNextLine()也一样
image-20210629220103390

nextLine()

package com.kimtanyo.scanner;
import java.util.Scanner;
public class Demo02 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("使用nextLine()方式接收:");

        if (scanner.hasNextLine()){
            String str = scanner.nextLine();
            System.out.println("输出的内容为:"+str);
        }
        scanner.close();
    }
}
image-20210629221431151

next()与nextLine()区别

next():

  • 一定要读取到有效字符后才可以结束输入。
  • 对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
  • 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
  • next() 不能得到带有空格的字符串。

nextLine():

  • 以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
  • 可以获得空白。
package com.kimtanyo.scanner;
import java.util.Scanner;
public class Demo03 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("请输入数据:");
		// 在确定输入源只有键盘、希望读取字符串(而不是数字)的情况下,可以不用验证hasNextLine()
        //一般实际使用nextLine()比较多,较少使用next()
        String str = scanner.nextLine();

        System.out.println("输出的内容为:"+str);

        scanner.close();
    }
}

close()

每次读入操作进行完毕要记得关闭Scanner对象,否则会持续占用内存

语法:

scanner.close();

注意:Scanner的数据流关闭一次就打不开了,在close之后再new Scanner()也不能打开,要避免在close之后再进行任何nextXxx()的读入操作

Scanner进阶使用

如果希望读取数字类型的数据,可以使用nextInt()、nextFloat()、nextDouble()等方法

注意:nextInt()、nextFloat()、nextDouble()等方法的读取也是会被空格隔断的,不一定非得回车

注意:数字类型数据一定要验证hasNextXxx(),否则类型不对会报错,字符串可以不验证hasNextLine()因为起码不会报错

package com.kimtanyo.scanner;
import java.util.Scanner;
public class Demo04 {
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);

        int i = 8;
        float f = 0.0f;
        System.out.println("请输入整数:");
        if (scanner.hasNextInt()){
            i = scanner.nextInt();
            System.out.println("整数数据:" + i);
        } else {
            System.out.println("输入的不是整数数据!");
        }
        System.out.println("请输入小数:");
        
		// scanner.nextLine();// 空读取一遍不赋值,清空缓存区
        
        if (scanner.hasNextFloat()){
            f = scanner.nextFloat();
            System.out.println("小数数据:"+f);
        } else{
            System.out.println("输入的不算小数数据!");
        }

        scanner.close();
    }
}

image-20210629224114899

Scanner是一个扫描器,他接收数据是去内存中一块缓冲区扫描并读入数据的,而我们在控制台输入的数据也是被先存到缓冲区中等待描扫读取

hasNextInt()的作用是先判断缓冲区内是否还有整型数据,若有则直接返回true,否则陷入block等待控制台输入,再判断输入是否为整型,若是返回true,否则返回false

nextInt()的作用是扫描读取缓冲区内的整型数据,读取后缓冲区清空

这里由于hasNextInt()判断缓冲区中存放的不是整型,所以返回false,没有通过nextInt()进行扫描读取,所以45.2继续被存放在缓冲区,然后hasNextFloat()发现缓冲区内还有单精度浮点数据,所以直接返回true,不会陷入block不需要控制台输入,nextFloat()读取清空了缓存区的45.2

解决方法:在验证hasNextFloat()前加上一句scanner.nextLine();,读取缓冲区但是不赋值,即可清空缓冲区

image-20210629232121293
package com.kimtanyo.scanner;

import java.util.Scanner;

public class Demo05 {
    public static void main(String[] args) {
        // 我们可以输入多个数字,并求总和与平均数,每输入一个数字用回车确认,通过非数字来结束输入并输出执行结果
        Scanner scanner = new Scanner(System.in);

        // 和
        double sum = 0;
        // 计算输入了多少个数字
        int m = 0;

        //通过循环判断是否还有输入,并在里面对每一次进行求和和统计
        while (scanner.hasNextDouble()){
            double x =scanner.nextDouble();
            // m = m + 1;
            m++;
            sum = sum + x;
            System.out.println("你输入了第" + m + "个数据,当前结果sum = " + sum);
        }
        System.out.println(m+"个数的和为"+sum);
        System.out.println(m+"个数的平均值为"+sum/m);
    }
}

image-20210629232313477

顺序结构

Java的基本结构激素顺序结构,除非特别指明,否则就按照顺序一句一句执行

顺序结构是最简单的算法结构

语句与语句之间,框和框之间是从上到下的顺序进行的,他是由若干个一次进行的处理步骤组成的,他是任何一个算法都离不开的一种基本算法结构

package com.kimtanyo.structure;

public class SequentialStructure {
    public static void main(String[] args) {
        System.out.println("hello1");
        System.out.println("hello2");
        System.out.println("hello3");
        System.out.println("hello4");
    }
}

If选择结构

if单选择结构

语法:

if(boolean表达式){
    如果为boolean表达式为true将执行的语句
}

例子:

package com.kimtanyo.structure;
import java.util.Scanner;

public class IfDemo01 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入内容:");
        String s = scanner.nextLine();

        // equals:判断字符串是否相等
        if (s.equals("Hello")){
            System.out.println(s);
        }
        System.out.println("End");
        scanner.close();
    }
}

注意:不能用==来判断字符串是否相等,因为==会判断字符串的内存地址是否相同,例子:为什么不能使用'=='判断字符串是否相等

if双选择结构

语法:

if (boolean表达式){
    //如果boolean表达式的值为true
}else{
    //如果boolean表达式的值为false
}

例子:

package com.kimtanyo.structure;
import java.util.Scanner;
public class IfDemo02 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入成绩:");
        int score = scanner.nextInt();

        if (score>=60){
            System.out.println("及格");
        }else{
            System.out.println("不及格");
        }

        scanner.close();
    }
}

if多选择结构

语法:

if (boolean表达式1){
    //如果boolean1表达式的值为true
}else if(boolean表达式2){
    //如果boolean2表达式的值为true,且boolean1表达式的值为false
}else if(boolean表达式3){
    //如果boolean3表达式的值为true,且之前的boolean表达式的值都为false
}else{
    //如果以上boolean表达式的值都为false
}

例子:

package com.kimtanyo.structure;
import java.util.Scanner;
public class IfDemo03 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入成绩:");
        int score = scanner.nextInt();

        if (score==100){
            System.out.println("恭喜满分");
        }else if(score<100 && score>=90){
            System.out.println("A级");
        }else if(score<90 && score>=80){
            System.out.println("B级");
        }else if(score<80 && score>=70){
            System.out.println("C级");
        }else if(score<70 && score>=60){
            System.out.println("D级");
        }else if(score<60 && score>=0){
            System.out.println("不及格");
        }else{
            System.out.println("成绩不合法");
        }

        scanner.close();
    }
}

嵌套的if结构

语法:

if (boolean表达式1){
    // 如果boolean表达式1的值为true
    if (boolean表达式2){
        // 如果boolean表达式2的值为true
    }
}

例子:二分法查找0-100之中的数字

Switch选择结构

多选择结构还有一个实现方式就是switch case语句

switch case语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支

  • switch语句的变量类型可以是byte、short、int或者char
  • 从JavaSE7开始,switch也支持字符串String类型
  • case标签必须为字符串常量或字面量(字面量即基本数据类型赋值后的量)
switch (variable) {
    case value :
        // 语句
        break;  // 可选
    case value :
        // 语句
        break;  // 可选
    // case语句数量任意
    default :  // 可选
        // 语句
}

其中default语句是给不符合所有case条件的情况提供执行语句

其中break语句的作用是提前跳出选择结构,不执行后面语句

如果满足variable==value的case内没有break语句,则除了执行该case内的语句外,还会执行该case后面所有case的语句,不需要判断value

通常每个case中都要写上break语句

例子:

package com.kimtanyo.structure;

public class SwitchDemo01 {
    public static void main(String[] args) {
        //switch匹配一个具体的值
        char grade = 'B';
        switch (grade) {
            case 'A':
                System.out.println("优秀");
                break;
            case 'B':
                System.out.println("良好");
            case 'C':
                System.out.println("及格");
                break;
            case 'D':
                System.out.println("再接再厉");
                break;
            case 'E':
                System.out.println("挂科");
                break;
            default:
                System.out.println("未知等级");

        }
    }
}

这种没有break语句导致后续case内语句也执行的现象称为case穿透现象

image-20210630002858903

IDEA反编译

String类型例子:

package com.kimtanyo.structure;

public class SwitchDemo02 {
    public static void main(String[] args) {
        String name = "kimtanyo";
        // JDK7的新特性,变量可以是字符串
        // 字符的本质还是数字

        // 反编译:  java文件--编译--class字节码文件--反编译(IDEA或其他反编译工具)--java文件

        switch (name){
            case "ccd":
                System.out.println("Autonym");
                break;
            case "kimtanyo":
                System.out.println("Alias");
                break;
            default:
                System.out.println("Error");
        }
    }
}

通过反编译(IDEA)查看class字节码文件的源码:

image-20210701180700169

打开编译器输出路径,找到class文件

image-20210701180755259

复制到scr路径中对应的java文件旁边

image-20210701180936408

在IDEA中可以发现src路径下的java文件旁出现了class文件,打开可以发现

image-20210701181439305

jdk7开始支持String对象作为switch的变量的原因就是,它通过编译实际判断的是对象的哈希值

While循环详解

语法:

while (布尔表达式){
    // 循环内容
}

只要布尔表达式为true,循环就会一直执行下去

大多数情况下是会让循环停止下来的,因此需要一个让表达式失效的方式来结束循环

例子:

package com.kimtanyo.structure;

public class WhileDemo01 {
    public static void main(String[] args) {
        // 输出0~100
        int i =0;
        while (i<100){
            i++;
            System.out.println(i);
        }
    }
}

少数情况需要循环一直执行,比如服务器的请求响应监听

例子:

package com.kimtanyo.structure;

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

        while (true){
            // 等待客户端连接
            // 定时检查
            // ……
        }
    }
}

循环条件一直为true就会造成无限循环(死循环),在正常的业务编程中应该尽量避免死循环,会影响程序性能或者造成程序卡死崩溃

例子:

package com.kimtanyo.structure;

public class WhileDemo03 {
    public static void main(String[] args) {
        // 计算1+2+…+100=?

        int i =0;
        int sum =0;
        while (i<=100){
            sum+=i;
            i++;
        }

        System.out.println(sum);

    }
}
// 5050

DoWhile循环

对于while语句而言,如果不满足条件,则不能进入循环,但有时候我们需要即使不满足条件,也至少执行一次

do…while循环和while循环类似,不同的是,do…while循环至少会执行一次

实际就是循环体和循环条件的先后区别

语法:

do {
    // 代码语句
}while (布尔表达式);
  • While是先判断后执行,DoWhile是先执行后判断
  • DoWhile总是保证循环体至少会被执行一次,这是他们的主要差别

例子:

package com.kimtanyo.structure;

public class DoWhileDemo01 {
    public static void main(String[] args) {
        int i =0;
        int sum = 0;

        do {
            sum += i;
            i++;
        }while (i<=100);

        System.out.println(sum);
    }
}
// 5050

例子:

package com.kimtanyo.structure;

public class DoWhileDemo02 {
    public static void main(String[] args) {
        int a =0;
        while (a<0){
            System.out.println(a);
            a++;
        }
        System.out.println("===========");
        do {
            System.out.println(a);
            a++;
        }while (a<0);
    }
}
// ===============
// 0

For循环详解

for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构

for循环执行的次数是在执行前就确定的

语法格式如下:

for (初始化; 布尔表达式; 更新) {
    // 代码语句
}

例子:

package com.kimtanyo.structure;

public class ForDemo01 {
    public static void main(String[] args) {
        int a= 1; // 初始化条件

        while (a<=100) { // 条件判断
            System.out.println(a); // 循环体
            a += 2; // 迭代
        }

        System.out.println("while循环结束!");

        // 初始化 条件判断 迭代
        for  (int i=1;i<=100;i+=2){
            System.out.println(i);
        }


        System.out.println("for循环结束!");
    }
}

for循环的逻辑顺序是:先初始化初始化循环变量,再判断循环条件,执行循环体,最后迭代循环变量

关于for循环有以下几点说明:

  • 最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。

  • 然后,检测布尔表达式的值。如果为true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。检测也可以是空语句。

  • 执行一次循环后,更新循环控制变量(迭代因子控制循环变量的增减)。更新循环控制变量也可以是空语句。

  • 再次检测布尔表达式,循环执行上面的过程。

三处空语句的应用场景:

  • 初始化为空语句:可能在前面已经定义了循环变量的场景
  • 条件检测为空语句:可能在想在循环体内执行循环条件判断然后break跳出,或者想构造死循环
  • 迭代为空语句:可能循环体内迭代了循环变量,或者想构造死循环

例子:

package com.kimtanyo.structure;

public class ForEmptyDemo {
    public static void main(String[] args) {
        int i=0;
        for ( ; ; ) {
            System.out.println(i);
            i++;
            if (i>5){
                break;
            }
        }
    }
}

image-20210702024330032

练习1:计算0到100之间的奇数与偶数的和

package com.kimtanyo.structure;

public class ForDemo02 {
    public static void main(String[] args) {
        // 练习1:计算0到100之间的奇数与偶数的和

        int oddSum = 0;
        int evenSum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i%2!=0) {
                oddSum += i;
            }else {
                evenSum += i;
            }
        }
        System.out.println("奇数的和:"+oddSum);
        System.out.println("偶数的和:"+evenSum);
    }
}
// 奇数的和:2500
// 偶数的和:2550

练习2:用while或for循环输出1-1000之间能被5整除的数,并且每行输出3个

package com.kimtanyo.structure;

public class ForDemo03 {
    public static void main(String[] args) {
        // 练习2:用while或for循环输出1-1000之间能被5整除的数,并且每行输出3个

        for (int i = 0; i <= 1000; i++) {
            if (i%5 == 0) {
                System.out.print(i+"\t");
            }
            if (i%(5*3) == 0) {
                System.out.print("\n");
                // 也可以System.out.println();
            }
        }
        
    }
}

扩展:println、print和printf

println():输出内容后换行

print():输出内容后不换行

printf():格式化输出

printf的格式控制的完整格式:
% - 0 m.n l或h 格式字符
下面对组成格式说明的各项加以说明:
①%:表示格式说明的起始符号,不可缺少。
②-:有-表示左对齐输出,如省略表示右对齐输出。
③0:有0表示指定空位填0,如省略表示指定空位不填。
④m.n:m指域宽,即对应的输出项在输出设备上所占的字符数。N指精度。用于说明输出的实型数的小数位数。为指定n时,隐含的精度为n=6位。
⑤l或h:l对整型指long型,对实型指double型。h用于将整型的格式字符修正为short型。
------------------------------------
格式字符
格式字符用以指定输出项的数据类型和输出格式。
d格式:用来输出十进制整数。有以下几种用法:
%d:按整型数据的实际长度输出。
%md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。
%ld:输出长整型数据。
②o格式:以无符号八进制形式输出整数。对长整型可以用"%lo"格式输出。同样也可以指定字段宽度用“%mo”格式输出。
③x格式:以无符号十六进制形式输出整数。对长整型可以用"%lx"格式输出。同样也可以指定字段宽度用"%mx"格式输出。
④u格式:以无符号十进制形式输出整数。对长整型可以用"%lu"格式输出。同样也可以指定字段宽度用“%mu”格式输出。 //不可使用 //在实践中没有运行出来。
⑤c格式:输出一个字符。
s格式:用来输出一个串。有几中用法
%s:例如:printf("%s", "CHINA")输出"CHINA"字符串(不包括双引号)。
%ms:输出的字符串占m列,如字符串本身长度大于m,则突破获m的限制,将字符串全部输出。若串长小于m,则左补空格。
%-ms:如果串长小于m,则在m列范围内,字符串向左靠,右补空格。
%m.ns:输出占m列,但只取字符串中左端n个字符。这n个字符输出在m列的右侧,左补空格。
%-m.ns:其中m、n含义同上,n个字符输出在m列范围的左侧,右补空格。如果n>m,则自动取n值,即保证n个字符正常输出。
f格式:用来输出实数(包括单、双精度),以小数形式输出。有以下几种用法:
%f:不指定宽度,整数部分全部输出并输出6位小数。
%m.nf:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。
%-m.nf:输出共占n列,其中有n位小数,如数值宽度小于m右端补空格。
------------------------------------
关于printf函数的进一步说明:
如果想输出字符"%",则应该在“格式控制”字符串中用连续两个%表示,如:
printf("%f%%", 1.0/3);
输出0.333333%。
------------------------------------
对于单精度数,使用%f格式符输出时,仅前7位是有效数字,小数6位.
对于双精度数,使用%lf格式符输出时,前16位是有效数字,小数6位.
######################################

对于m.n的格式还可以用如下方法表示(例)
char ch[20];
printf("%*.*s\n",m,n,ch);
前边的*定义的是总的宽度,后边的定义的是输出的个数。分别对应外面的参数m和n 。我想这种方法的好处是可以在语句之外对参数m和n赋值,从而控制输出格式。

for循环快捷补全

IDEA中for循环有一种快捷语法补全:输入循环次数.for然后选择fori或者forr回车,即可补全for循环语句

其中fori补全是

for (int i=0;i<循环次数;i++) {}

forr补全是

for (int i=循环次数;i>0;i--) {}
image-20210702023316315 image-20210702023330855

另一种方式是输入fori然后按Tab键也可以补全

打印九九乘法表

此节是for循环的一种较难练习

一般这种多重for循环的题目先写出某一个固定内循环,然后找逻辑规律构建外循环

package com.kimtanyo.structure;
/*
1*1=1	
1*2=2	2*2=4	
1*3=3	2*3=6	3*3=9	
1*4=4	2*4=8	3*4=12	4*4=16	
1*5=5	2*5=10	3*5=15	4*5=20	5*5=25	
1*6=6	2*6=12	3*6=18	4*6=24	5*6=30	6*6=36	
1*7=7	2*7=14	3*7=21	4*7=28	5*7=35	6*7=42	7*7=49	
1*8=8	2*8=16	3*8=24	4*8=32	5*8=40	6*8=48	7*8=56	8*8=64	
1*9=9	2*9=18	3*9=27	4*9=36	5*9=45	6*9=54	7*9=63	8*9=72	9*9=81	
 */
public class ForDemo04 {
    public static void main(String[] args) {
        // 1.先打印第一列
        // 2.把固定的1再用一个循环包起来
        // 3.去掉重复项,i<=j
        // 4.调整样式

        for (int j = 1; j <= 9; j++) { // j是列索引(第几行)
            for (int i = 1; i <= j; i++) { // i是行索引(第几列)
                System.out.print(i+"*"+j+"="+(j*i)+"\t");
            }
            System.out.println();
        }

    }
}

增强for循环

这里只是先简单介绍一下,之后数组部分会重点使用

增强for循环语法格式如下:

for (声明语句 : 表达式) {
    // 代码句子
}

声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配,其作用域限定在循环语句块,其值与此时数组元素的值相等

表达式:表达式是要访问的数组名,或者是返回值为数组的方法

package com.kimtanyo.structure;

public class AugmentedForDemo01 {
    public static void main(String[] args) {
        int[] numbers = {10,20,30,40,50}; // 定义了一个数组

        
        for (int i = 0; i < 5; i++) {
            System.out.println(numbers[i]);
        }

        System.out.println("===========");

        // 遍历数组的元素
        for (int x:numbers){
            System.out.println(x);
        }
    }
}
// 这两个循环是完全等价的(不仅是效果一致)
image-20210702033112460

break、continue、goto

break在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。(break语句也在switch语句中使用)

package com.kimtanyo.structure;

public class BreakDemo {
    public static void main(String[] args) {
        int i = 0;
        while (i<100){
            i++;
            System.out.println(i);
            if (i==30){
                break;
            }
        }
        System.out.println("循环结束");
    }
}

image-20210702034509027

continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定。

package com.kimtanyo.structure;

public class ContinueDemo {
    public static void main(String[] args) {
        int i = 0;
        while (i<100){
            i++;
            if (i%10==0){
                System.out.println();
                continue;
            }
            System.out.print(i);
        }
    }
}

image-20210702034605784

关于goto关键字

  • goto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字,但并未在语言中得到正式使用;Java没有goto。然而,在break和continue这两个关键字的身上,我们仍然能看出一些goto的影子---带标签的break和continue

  • “标签”是指后面跟一个冒号的标识符,例如:label:

  • 对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。但是不建议使用。

package com.kimtanyo.structure;

public class LabelDemo {
    public static void main(String[] args) {
        // 打印101-150之间的所有质数

        int count = 0;
        flag:for (int i=101;i<=150;i++){
            for(int j=2;j<=(int)Math.sqrt(i);j++){ // 合数i的除1以外的最小因数j是质数且j<=sqrt(i)
                if (i%j==0){
                    continue flag;
                }
            }
            System.out.print(i+"\t");
        }

        System.out.println();
        System.out.println("============ continue 和 break 代替 label =============");

        int flag = 0;
        for (int i=101;i<=150;i++){
            for(int j=2;j<=(int)Math.sqrt(i);j++){ // 合数i的除1以外的最小因数j是质数且j<=sqrt(i)
                if (i%j==0){
                    flag = 1;
                    break;
                }
            }
            if (flag == 1){
                flag = 0;
                continue;
            }
            System.out.print(i+"\t");
        }
    }
}

image-20210702041214669

java中唯一使用标签的地方是在循环之前,这里只要判断出现了因数,那么就跳到标签所在的外循环并先补上本次的迭代再进入下一次循环,实际上完全可以用break+continue的组合代替

打印三角形及Debug

打印三角形5行:

package com.kimtanyo.structure;

public class TestDemo {
    public static void main(String[] args) {
        // 打印三角形 5行
        /*
              *
             ***
            *****
           *******
          *********
         */
        for (int i = 1; i <= 5; i++) {
            for (int j = 4; j >= i-1; j--) {
                System.out.print(" ");
            }
            for (int j = 1; j <= i; j++){
                System.out.print("*");
            }
            for (int j = 0; j < i-1; j++){
                System.out.print("*");
            }
            System.out.println();
        }

    }
}

上面代码思路是区分三角形为三部分,如图所示:

屏幕截图 2021-07-02 042748

Debug

Debug可以有效帮助理解代码和纠错

image-20210702043725266
image-20210702043819527

Java方法

什么是方法

例如System.out.println()是系统类System中out对象的println()方法

Java方法是语句的集合,它们在一起执行一个功能

  • 方法是解决一类问题的步骤的有序组合
  • 方法包含于类或对象中
  • 方法在程序中被创建,再其他地方被引用

设计方法的原则:方法的本意是功能块,就是实现某个功能的语句块的集合,我们设计方法的时候,最好保持方法的原子性,就是一个方法只完成1个功能,这样利于我们后期的扩展

方法的命名规则是小驼峰命名法

package com.kimtanyo.method;

public class Demo01 {
    // main方法尽量保持简洁干净,把公共部分放到外面的方法中
    public static void main(String[] args) {
        int sum = add(1, 2);
        System.out.println(sum);

        test();
    }

    // 加法
    public static int add(int a, int b){
        return a+b;
    }

    // ForDemo03
    public static void test(){
        for (int i = 0; i <= 1000; i++) {
            if (i%5 == 0) {
                System.out.print(i+"\t");
            }
            if (i%(5*3) == 0) {
                System.out.print("\n");
                // 也可以System.out.println();
            }
        }

    }
}

这里为了方便调用,因此先暂时统一定义static方法,后续讲到类的时候会解释为什么要加static关键字

扩展:Java 形参和实参的区别

形参 :就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。 形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。 因此,形参只在方法内部有效,针对基本数据类型的改动无法影响到方法外。

实参 :就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。

注意 :在值传递调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。而在引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上

值传递 :方法调用时,基本数据类型的实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy, 此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。

引用传递 :也称为地址传递、址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址,在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用类型数据的操作将会影响到实际对象

注意 :类似的 String, Integer, Float, Double, Short, Byte, Long, Character基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如 Integer内部有一个 value 来记录 int 基本类型的值,但是自身没有提供修改它的方法,所以对形参的改变不会影响到实参的基本包装类型对象

注意:Java只有值传递,没有引用传递 (只不过对于对象参数,值的内容是对象的地址)

方法的定义

Java的方法类似于其它语言的函数,是一段用来完成特定功能的代码片段

一般情况下,定义一个方法包含以下语法:

修饰符 返回值类型 方法名(参数类型 参数名){
    ...
    方法体
    ...
    return 返回值;
}

方法包含一个方法头和一个方法体。下面是一个方法的所有部分:

  • 修饰符:修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型(没有static修饰的方法需要定义该类的一个对象实例,在实例中才能调用没有static修饰的方法 e.g.
  • 返回值类型:方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作,但没有返回值。在这种情况下,returnValue Type 是关键字void。
  • 方法名:是方法的实际名称。方法名和参数表共同构成方法签名。
  • 参数类型:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。
    • 形参:在方法被调用时用于接收外界输入的数据。
    • 实参:调用方法时实际传给方法的数据。
  • 方法体:方法体包含具体的语句,定义该方法的功能。
package com.kimtanyo.method;

public class Demo02 {
    public static void main(String[] args) {
        System.out.println(max(3,3));
    }
    
    // 比大小
    public static int max(int num1, int num2){

        if (num1 == num2){
            System.out.println("num1 == num2");
            return 0; // 终止方法
        }
        if (num1>num2){
            return num1;
        }else{
            return num2;
        }

    }
}

return有另一个作用是终止方法,跳过方法定义中之后所有的语句,直接返回值或者空返回return;,空返回需要声明void

扩展:Java中只有值传递

注意:Java只有值传递,没有引用传递

只不过对于对象参数,复制的值的内容是对象的地址

(笔试重点考察)

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

  • 对于本身提供改变自身值的方法的引用类型变量,的确对形参应用这种自身提供的改变值方法可以改变实参的值,但这是因为实际改变的是地址

  • 而如果对于本身不提供改变自身值的方法的引用类型变量,企图直接通过赋值的方式改变形参值是不会改变实参的值的,因为赋值会给形参分配新的内存空间和地址,相当于新建了一个类变量的地址映射,当然不会改变原地址对应的实参(例如String, Integer, Float, Double, Short, Byte, Long, Character等基本包装类型变量)

例如:形参builder,实参sb是StringBuilder("iphone")

img

方法中builder = new StringBuilder("ipad");之后

img

两个经典例子:

1. 为什么说Java中只有值传递 - CSDN

2. Java 到底是值传递还是引用传递?- 知乎

方法的重载

重载(Overload)就是在一个类中,有相同的函数名称,但形参不同的函数。

方法的重载的规则(笔试常考)

  • 方法名称必须相同
  • 参数列表必须不同(个数不同、或类型不同、参数排列顺序不同等)
  • 方法的返回类型可以相同也可以不相同
  • 仅仅返回类型不同不足以成为方法的重载

实现理论

方法名称相同时,编译器会根据调用方法的参数个数、参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。

package com.kimtanyo.method;

public class Demo02 {
    public static void main(String[] args) {
        System.out.println(max(3.0,10.0));
    }

    public static double max(double num1, double num2){
        
        if (num1>num2){
            return num1;
        }else{
            return num2;
        }
    }

    public static int max(int num1, int num2, int num3){
        
        if (num1>=num2 && num2>=num3){
            return num1;
        }else if(num1>=num2 && num2<num3){
            if (num1>=num3){
                return num1;
            }else {
                return num3;
            }
        }else if(num1<num2 && num2>=num3){
            return num2;
        }else {
            return num3;
        }
    }

    // 比大小
    public static int max(int num1, int num2){

        if (num1 == num2){
            System.out.println("num1 == num2");
            return 0; // 终止方法
        }
        if (num1>num2){
            return num1;
        }else{
            return num2;
        }
    }
}

命令行传参

(了解为主,用处不大)

有时候会需要先不给参数,直到需要运行程序的时候再传递给它参数,这要靠传递命令行参数给main()函数实现

package com.kimtanyo.method;

public class Demo03 {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("args["+i+"]"+args[i]);
        }
    }
}

image-20210702211913067

可以看到main方法中的参数args本来是空的字符串数组,这里在命令行里面运行的时候才给了具体参数{"this","is","kimtanyo"}

另外注意,如果java文件是在包机制下有package声明语句,编译为class文件后,是不可以直接在class文件的当前路径下用java class文件名来执行class文件的,需要利用包机制,在src下(也就是package声明语句的上一级)执行java package语句.class文件名命令

可变参数

可变参数,也叫不定项参数

  • JDK1.5开始,Java支持传递同类型的可变参数给一个方法
  • 在方法声明中,在指定参数类型后加一个省略号(…)
  • 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数,任何普通的参数必须在它之前声明

其实就是为了解决多个重载过于麻烦的问题

e.g.

package com.kimtanyo.method;

public class Demo04 {
    public static void main(String[] args) {
        Demo04 demo04 = new Demo04();
        demo04.test(1,2,3,4,5);

    }
    public void test(int... i){
        System.out.println(i);
        System.out.println(i[0]);
        System.out.println(i[1]);
        System.out.println(i[2]);
        System.out.println(i[3]);
        System.out.println(i[4]);
    }

}
// 这里直接打印i是打印数组i的内存地址,多个实参传递给可变参数会被认为是一个数组
// 没有static修饰的方法需要定义该类的一个对象实例,在实例中才能调用没有static修饰的方法 
package com.kimtanyo.method;

public class Demo4_1 {
    public static void main(String[] args) {
        printMax(34, 3, 3, 2, 56.5);
        printMax(new double[]{1,2,3});

    }
    public static void printMax(double... numbers){
        if (numbers.length == 0){
            System.out.println("No argument passed");
            return; // 提前结束
        }

        double result = numbers[0];

        // 找最大值
        for (int i = 1; i < numbers.length; i++) {
            if (numbers[i] > result){
                result = numbers[i];
            }
        }
        System.out.println("The max value is "+result);
    }
}

递归(难点&&笔试高频)

A方法调用B方法,我们很容易理解
递归就是:A方法调用A方法,就是自己调用自己

利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。

递归结构包括两个部分:

  • 递归头:什么时候不调用自身方法。如果没有头,将陷入死循环。
  • 递归体:什么时候需要调用自身方法。

递归过程分为:边界条件(递归头)、前阶段(调用)和返回阶段(回溯)

例子:

package com.kimtanyo.method;

public class Demo06 {
    public static void main(String[] args) {
        System.out.println(f(5));
    }

    //利用递归构造阶乘函数
    public static int f(int n){
        if (n==1){
            return 1; // 递归头
        }else{
            return n*f(n-1); // 递归体
        }
    }
}


image-20210703012950659

实际上,Java方法的调用是使用栈机制的,main()方法在最底下,每次调用一个方法就往上堆一层,直到main()方法执行完毕,释放全部内存。如果递归次数太多,会导致运行时间非常久,或者栈空间不足,程序崩溃。参考:堆栈是个啥

实际自己写代码能不用递归就不用递归,只有对某些递归次数较低的问题可以用递归结构解决

这里讲的关于递归比较少,因为是以实际工作为目的,递归需要在LeetCode、数据结构和算法课上额外深入学习

作业:计算器

  • 写一个计算器,要求能计算加减乘除,并能够循环接收数据,且接收用户输入
  • 写4个方法,加减乘除
  • 利用循环+switch进行用户交互
  • 传递需要操作的两个数,并输出结果
package com.kimtanyo.method;
import java.util.Scanner;

public class CalculatorDemo {

    static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {

        for (int i=0; ;i++) {
            // 首次输入不询问是否继续计算
            if (i!=0){

                System.out.println("是否继续计算,Y/N");
                String flag = scanner.next();

                if (flag.equals("Y")){
                    calculator();
                }else if (flag.equals("N")){
                    break;
                }else {
                    System.out.println("请重新输入Y/N");
                }
            }else {
                calculator();
            }
        }
        scanner.close();

    }

    public static void calculator() {

        double a = scanner.nextDouble();
        String operator = scanner.next();
        double b = scanner.nextDouble();

        switch (operator) {
            case "+":
                System.out.println(add(a, b));
                break;
            case "-":
                System.out.println(minus(a, b));
                break;
            case "*":
                System.out.println(multiply(a, b));
                break;
            case "/":
                if (b==0) {
                    System.out.println("除数不能为0,请重新输入");
                    break;
                }
                System.out.println(divide(a, b));
                break;
            default:
                System.out.println("只支持四则运算,请重新输入");
        }

    }

    public static double add(double a, double b){
        return a+b;
    }
    public static double minus(double a, double b){
        return a-b;
    }
    public static double multiply(double a, double b){
        return a*b;
    }
    public static double divide(double a, double b){
        return a/b;
    }
}

注意:Scanner的数据流关闭一次就打不开了,在close之后再new Scanner()也不能打开,要避免在close之后再进行nextXxx()的读入操作

如果我们在每次循环的开头new Scanner()在循环的最后scanner.close(),就会报错

解决方法:Scanner对象不要放在循环里面,最好可以直接作为类变量放在main方法外面,全部调用完成再close即可

Java数组

什么是数组

  • 数组是相同类型数据的有序集合**(跟Python列表不一样)**
  • 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成
  • 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们

数组的声明和创建

  • 首先必须声明数组变量,才能在程序中使用数组(声明的时候不用给出数组长度)。下面是声明数组变量的语法:
dataType[] arrayRefVar; // 首选的方法
dataType arrayRefVar[]; // 效果相同,但不是首选方法
dataType[] arrayRefVar = null; // 效果相同,但有点啰嗦
  • Java语言使用new操作符来创建数组实例,分配内存空间给数组实例对象(需要给出数组的长度),语法如下:
arrayRefVar = new dataType[arraysize]; // 数组名必须在前面声明过类型
  • 一般情况下声明和创建一起写
dataType[] arrayRefVar = new dataType[arraysize]; // 声明+创建
  • 数组的元素是通过索引访问的,数组索引从0开始

  • 获取数组长度:

arrays.length

例子:

package com.kimtanyo.array;

public class ArrayDemo01 {
    // 变量类型 变量名字 = 变量值

    public static void main(String[] args) {
        int[] nums; // 1. 声明nums为int类型的数组

        nums = new int[10]; // 2. 创建nums数组,分配内存空间


        // 3. 给数组元素赋值,int类型默认为0
        nums[0] = 1;
        nums[1] = 2;
        nums[2] = 3;
        nums[3] = 4;
        nums[4] = 5;
        nums[5] = 6;
        nums[6] = 7;
        nums[7] = 8;
        nums[8] = 9;
        nums[9] = 10;

        System.out.println(nums[9]);

        // 计算所有元素的和
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }

        // 获取数组长度:array.length
        System.out.println("总和为:"+sum);
    }
}

也可以一句话int[] nums = new int[10];来声明和创建数组

三种初始化及内存分析

Java内存分析

(仅简单介绍,以后会详细讲)

image-20210704015329583

在声明数组的时候仅在栈中存入引用的数组在堆里面的地址,数组实际并不存在,在创建数组实例的时候才在堆里面分配内存空间给这个地址存放数组

参考:堆栈是个啥

image-20210704023413629

三种初始化

静态初始化

创建的时候就赋值了

int[] a = {1,2,3};
Man[] men = {new Man(1,1), new Man(2,2)}

动态初始化

创建之后再对元素额外赋值

int[] a = new int[2];
a[0] = 1;
a[1] = 2;

数组的默认初始化

数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。

  • 整型数组的默认值是0
  • 浮点数组的默认值是0.0
  • 布尔数组默认值是false
  • char数组默认值是''
  • String数组默认是null
  • 类数组按照自身定义的内容初始化
package com.kimtanyo.array;

public class ArrayDemo02 {
    public static void main(String[] args) {
        // 静态初始化:创建+赋值
        int[] a = {1,2,3,4,5,6,7,8};

        System.out.println(a[a.length-1]);

        Man[] men = {new Man(), new Man()};// 以后讲类会说

        //动态初始化:包含默认初始化
        int[] b = new int[10];
        b[0]=10;

        System.out.println(b[0]);
        System.out.println(b[1]);
        
    }
}

下标越界及小结

数组的基本特点

  • 其长度是确定的。数组一旦被创建,它的大小就是不可以改变的(跟Python不一样)
  • 元素必须是相同类型,不允许出现混合类型**(跟Python不一样)**
  • 数组中的元素可以是任何数据类型,包括基本类型和引用类型
  • 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量
  • 数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的

数组边界

下标的合法区间:[0, length-1],如果越界就会标错

public static void main(String[] args) {
    int[] a = new int[2];
    System.out.println(a[2]);
}
// ArrayIndexOutOfBoundsException:数组下标越界异常

小结:

  • 数组是相同数据类型(数据类型任意)的有序集合
  • 数组也是对象,数组元素相当于对象的成员变量
  • 数组长度是确定的,不可变的,如果越界则报错:ArrayIndexOutOfBoundsException

数组使用

  • For-Each循环(增强for循环)
  • 数组作方法入参
  • 数组作返回值
package com.kimtanyo.array;

public class ArrayDemo04 {
    public static void main(String[] args) {
        int[] arrays = {1,2,3,4,5};

        // JDK1.5以后 增强for循环没有下标
        for(int item: arrays){
            System.out.println(item);
        }

        printArray(arrays);
        int[] result = reverse(arrays);
        printArray(result);

    }

    // 打印数组元素
    public static void printArray(int[] arrays) {
        for (int i = 0; i < arrays.length; i++) {
            System.out.print(arrays[i]+" ");
        }
        System.out.println();
    }

    // 反转数组
    public static int[] reverse(int[] arrays){
        int[] result = new int[arrays.length];

        for (int i = 0, j = result.length-1; i < result.length && j >= 0; i++, j--){
            result[j]=arrays[i];
        }
        return result;
    }
}

注意:实际上对于数组,普通循环用得较多,for-each大多用来打印

多维数组

多维数组即数组的嵌套,数组的元素依然是数组

例如二维数组

int a[][] = new int[2][5];

相当于创建一个2行5列的矩阵

注意:数组的每个元素数组的长度不一定非要一样,静态初始化可以写出不一样的长度例如int[][] array = {{1,2},{1,2,3}};

package com.kimtanyo.array;

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

        // [4][2]
        /*
         * 1,2 array[0]
         * 2,3 array[1]
         * 3,4 array[2]
         * 4,5 array[3]
         * */
        int[][] array = {{1, 2}, {2, 3, 4}, {3, 4, 5, 6}, {4, 5, 6, 7, 8}};

        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array[i].length; j++) {
                System.out.println(array[i][j]);
            }
        }
    }
}

Arrays类

数组的工具类java.util.Arrays

由于数组对象本身并没有什么方法可以供我们调用,但API中提供了一个工具类Arrays供我们使用,从而可以对数据对象进行一些基本的操作。

查看JDK帮助文档

image-20210720015139043

Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用,而“不用“使用对象来调用(注意:是“不用“而不是“不能")

常用功能

  • 给数组赋值:通过 fill 方法。
  • 对数组排序:通过 sort 方法,默认按升序。
  • 比较数组:通过 equals 方法比较数组中元素值是否相等。
  • 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
  • 创建副本:通过 clone 方法,返回实例的一个克隆

将指定的int值分配给指定的int数组的每个元素。

package com.kimtanyo.array;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.IntStream;
import java.util.stream.Stream;

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

        int[] a = {1,2,3,4,9090,3213312,4214,32,5,25};
        int[] bak = a.clone();

        System.out.println(a);// 输出的是哈希值 [I@1b6d3586

        // 打印数组元素Arrays.toString()
        System.out.println(Arrays.toString(a));

        // 排序算法
            //升序(默认)
            System.out.println("升序");
            int[] tmp = a.clone();
            Arrays.sort(a);
            System.out.println(Arrays.toString(a));
            Arrays.sort(tmp,4,7);// 只排序tmp[4]到tmp[6],末尾下标[7]是不包括的
            System.out.println(Arrays.toString(tmp));

            //降序(将int数组先转化为Integer数组,再用Collections.reverseOrder()参数)
            //也可以直接重新初始化赋值Integer数组,就可以省略转化步骤
            System.out.println("降序");
                //将int数组转换为Integer数组
                //先将int数组转换为数值流
                IntStream stream = Arrays.stream(a);
                //流中的元素全部装箱,转换为流 ---->int转为Integer
                Stream<Integer> integerStream = stream.boxed(); // <>是泛型的意思,指定类的成员变量类型
                //将流转换为数组
                Integer[] b = integerStream.toArray(Integer[]::new);
            Arrays.sort(b,Collections.reverseOrder());
            System.out.println(Arrays.toString(b));


        // 赋值
        System.out.println("赋值");
        int[] c = a.clone();
        int[] d = a.clone();
        Arrays.fill(c,0);
        System.out.println(Arrays.toString(c));
        Arrays.fill(d,4,6,0);// 只排序d[4]到d[5],末尾下标[6]是不包括的
        System.out.println(Arrays.toString(d));

        //比较数组元素是否相等
        System.out.println("比较数组元素是否相等");
        int[] standard = {1, 2, 3, 4, 5, 25, 32, 4214, 9090, 3213312};
        System.out.println(Arrays.equals(a,standard));// 只有每个元素都相等才返回true

        //查找数组元素
        System.out.println("查找数组元素");
        int found = Arrays.binarySearch(a,3213312);
        System.out.println(found);// 如果在数组内会返回索引下标
        System.out.println(Arrays.binarySearch(a, 6));// 如果不在数组内会返回(-应当插入的位置索引-1),也就是“应当排在的位置序号的负数”
    }

}

image-20210720030751923

冒泡排序

总共又八大排序方法,冒泡排序是其中最出名的

逻辑:两层循环,外层冒泡轮数,里层依次比较

我们看到嵌套循环,应该立马就可以得出这个算法的时间复杂度为O(n2)(因为循环次数为1+2+...+n1=n(n1)21+2+...+n-1=\frac{n(n-1)}{2}

思考:如何优化(没走完就已经排好序了的情况下,不需要再继续排序)

package com.kimtanyo.array;
import java.util.Arrays;
public class ArrayDemo07 {
    public static void main(String[] args) {

        int[] a = {1,4,98,34,68,32,21,2};
        int[] sort = sort(a);
        System.out.println(Arrays.toString(sort));

    }

    // 冒泡排序
    // 1. 比较数组中,两个相等的元素,如果第一个数比第二个数要大,我们就交换他们的位置
    // 2. 每一次比较,都会产生出一个最大,或者最小的数字
    // 3. 下一轮则可以少一次排序
    // 4. 依次循环,直到结束

    public static int[] sort(int[] array){
        // 临时变量
        int temp = 0;
        // 外层循环,判断我们这个要走多少次
        for (int i = 0; i < array.length-1; i++) {

            boolean flag = false;// 通过flag标识位减少没有意义的比较

            // 内层循环,比较判断两个数,如果第一个数比第二个数大,则交换位置
            for (int j = 0; j < array.length-1-i; j++) {
                if (array[j+1] < array[j]){ // 如果要降序排序只需要把这里的 < 改成 >
                    temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    flag = true;
                }
            }

            if (flag==false){
                break;
            }
        }
        return array;
    }
}

稀疏数组

需求:编写五子棋游戏中,有存盘退出和续上盘的功能

image-20210720181644770

分析问题:因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据

解决方法:稀疏数组

使用场景:当一个数组中大部分元素为0或者为同一值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方式是:

  • 记录数组一共有几行几列,有多少个不同值

  • 把具有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模

如下图:左边是原始数组,右边是稀疏数组

image-20210720182009828

第一个值保存的是其中不为0或者不为同一值的有效数字个数,第一个行列表示数组的首层索引长度是6,内层索引长度是7

例如上图五子棋游戏的稀疏数组就应该是:

[0] 11 11 2
[1] 1 2 1
[2] 2 3 2
package com.kimtanyo.array;

public class ArrayDemo08 {
    public static void main(String[] args) {
        // 1. 创建一个二维数组 11*11 0:没有棋子 1:黑棋 2:白棋

        int[][] array1 = new int[11][11];
        array1[1][2]=1;
        array1[2][3]=2;
        // 输出原始的数组
        System.out.println("输出原始的数组");
        for (int[] ints:array1) {
            for (int anInt : ints) {
                System.out.print(anInt + "\t");
            }
            System.out.println();
        }

        // 2. 转换为稀疏数组保存
        // 获取有效值的个数
        int sum = 0;
        for (int i = 0; i < array1.length; i++) {
            for (int j = 0; j < array1[i].length; j++) {
                if (array1[i][j]!=0){
                    sum++;
                }
            }
        }
        System.out.println("有效值个数: "+sum);

        // 创建一个稀疏数组
        int[][] array2 = new int[sum+1][3];
        array2[0][0]=array1.length;
        array2[0][1]=array1[0].length;
        array2[0][2]=sum;

        // 遍历二维数组给稀疏数组赋值
        int count = 0;
        for (int i = 0; i < array1.length; i++) {
            for (int j = 0; j < array1[i].length; j++) {
                if (array1[i][j]!=0){
                    count++;
                    array2[count][0] = i;
                    array2[count][1] = j;
                    array2[count][2] = array1[i][j];
                }
            }
        }
        
        // 输出稀疏数组
        System.out.println("稀疏数组");
        for (int i = 0; i < array2.length; i++) {
            System.out.println(array2[i][0]+"\t"
                    +array2[i][1]+"\t"
                    +array2[i][2]);
        }
        System.out.println("========================");

        // 3. 还原为二维数组
        // 读取稀疏数组
        int[][] array3 = new int[array2[0][0]][array2[0][1]];
        // 遍历稀疏数组给二维数组还原它的值
        for (int i = 1; i < array2.length; i++) { // 注意遍历从1开始,因为稀疏数组第1行是头部信息
                array3[array2[i][0]][array2[i][1]] = array2[i][2];
        }
        // 打印
        System.out.println("输出还原的二维数组");

        for (int[] ints : array3){
            for (int anInt : ints){
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
    }
}

输出原始的数组
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
有效值个数: 2
稀疏数组
11 11 2
1 2 1
2 3 2
输出还原的二维数组
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0