本人码云(建议pc端打开,手机可能会出现字体过小问题。)
本人会不定期在码云上更新一些算法或数据结构的基础程序,后期也会学习进阶程序,也会更新大厂的面试题。
如果您有好的建议和想法,欢迎在本人码云相应代码评论区评论哦。希望大家都能在编程这条路上越走越远。也祝看到这篇博客的人,能真正搞懂这方面知识,当然,若有错误,请提出改正,谢谢大家啦。
本人后续会在这里更新操作系统和计组的笔记,敬请期待!
有赞必回,评论必回,顺着网线回访!
Java数组
1.数组
(1) 什么是数组
数组本质上就是让我们能 “ 批量
” 创建相同数据类型
的变量。
一次定义N个相同数据类型的变量,我们就把这种结构称之为数组。
1、数组是一种引用数据类型
;
2、数组当中各个数据类型相同
;
3、数组的长度
在程序运行期间不可改变
。
数组也是一种数据结构
–>顺序表
数据结构:把一堆数字保存起来的结构就是数据结构
数据结构关心的是如何高效的读写数据。
我们之前创建两个整型变量:
int a = 10; int b = 20;
创建四个整形变量,亦可以一行行定义
假设我要定义一万个整型变量,这个时候我们就要使用数组
(2) 数组的创建与初始化:
1.数组的动态初始化
数据类型 [] 数组名称
= new 数据类型 []
{ 初始化数据 }
数据类型 [] 数组名称
= new 数据类型 [ num ](num代表当前数组的长度)
例:int [] arr
= new int [] { 1, 3, 5, 7, 9 };
在创建数组时,若没有使用大括号来初始化每个元素的值,每个元素都是该数据类型的
默认值
。
例:int [] arr
=new int [ 5 ];
// 0, 0, 0, 0, 0
2.数组的静态初始化
数据类型 [] 数组名称
= { 初始化数据 };
例:int [] arr
= { 1, 3, 5, 7, 9 };
静态初始化只是创建的一个语法塘。
语法糖:javac 编译之后,就是一个动态初始化。只存在编译期之前
的语法,编译器为了方便程序员编程
的简化写法。
先从idea的自带的控制台( 右击–> open in --> terminal)、输入javac命令、编译代码:
这一步会生成jvm可以识别的.class文件:
然后通过idea查看生成的class文件:
发现、这两种创建数组的方式、在JVM看来,其实是一样的。
其实、在Java中有很多语法糖、包括泛型 / 包装类的自动拆装箱…都是语法糖。
(3)数组的使用
1.数组长度
获取一个数组长度,使用数组名称 . length
int [] arr
= new int [5];
int [] a
= new int [] { 1, 3, 5, 7, 9 };
2.访问数组元素
数组名称 [ 要访问的元素
相较于
第一个元素的偏移量
]
数组名称 [元素的索引
]
例:要取第一个
元素:arr [0]
为啥要从索引 0 开始?
索引其实就是“ 偏移量 ”
相较于数组第一个元素
的单位长度
第一个元素与它本身
的偏移量为0
,故从0
开始
假设一层楼有10个宿舍,我要找第五个宿舍,首先你得找到第一个宿舍在哪。
数组在内存
中存储时,每个元素之间都是顺序存储
的
3.如果访问了一个数组索引中并不存在的元素会怎么样?
会报错!
ArrayIndexOutOfBoundException
(数组越界异常)
以后记住、如果运行文件报这个错,一定是访问了一个非法索引,这个索引在当前数组中根本不存在!
4.遍历数组
for循环:
for ( int i = 0; i <= arr.length - 1; i ++ )
{
System.out.print( arr[i] + ",");
//此时每个 i 表示的是数组的下标
//arr[i]确实拿到了每一个数组元素
}
for - each 循环(增强的for)
for ( int n : arr )
{
System.out.print( n + "," );
}
对于增强的 for 循环:
n 表示的是每一个数组元素、从第一个元素开始取值 =>
第一次、把第一个元素的值复制一份给 n,
第二次、把第二个元素的值复制一份给 n,
一直走下去,直到遍历结束
很显然,这种循环不能修改原数组的值
因为他每次只是值
的复制、并不是真正的通过栈中的引用指向堆中的数组地址。
(4)数组地址分析
JVM 把内存分为 6 个区域,我们这次只了解 “栈区” 和 “堆区”。
首先介绍栈区:
一个程序要想运行,必须要有
主类
和主方法
。首先、加载主类
(此时会有静态的属性同时加载、后面会讲)、加载完主类、让主方法进栈
。因为主方法作为每个程序的入口
,主方法不先进栈,这个程序就无法运行。主方法的调用就是在栈区
进行的,当执行到主方法的大括号
时,主方法执行结束
。每个方法的调用
过程,实际上就是一个栈帧的入栈以及出栈
过程。
栈是一种后进先出的结构,最后一次调用的方法,会首先被弹出栈。
其次,方法中有很多局部变量
和形参
,这些局部变量和形参都保存在栈中
。局部变量处于哪一个方法的大括号内部,就会在这个方法调用结束时,被销毁
。
疑惑:什么是实参、什么是形参?
实参
是位于主调函数
(由 a 方法去调用 b 方法、那么、a 就是主调函数)的局部变量
、实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值
,以便把这些值传送给形参。进入被调函数后,实参变量开始不能使用(因为进入了另一个方法内部)。形参和实参的功能是作数据传送
。发生函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送
。
由于实参和形参
都是局部变量
(位于方法内部),所以都存在于栈区
。
形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量。
接着介绍堆区:
JVM 的另一块内存区域称为 “
堆区
”,所有对象都在堆中存储
。这些对象包括:数组对象,类的实例化对象,接口的对象等等。
因为他们全部都是引用数据类型
、引用数据的本质
就是在栈区保存对象的引用
(这个引用存储的是位于堆区的那个对象的地址值
),在堆区保存对象本身
。
对于数组而言,在创建一个新的数组时,
例:int [] a = new int [] { 10, 20 };
,java语句的执行都是从右向左
。
首先观察右边,是一个new
语句,记住在java中,new出来的东西就是对象
,一定在堆当中,所以首先在堆内为这个数组分配空间,大小为8个字节,因为一个
int 型常量占 4 个字节大小,而这里刚好是一个长度为 2 的整型数组。
然后观察左边,发现在栈中创建了一个 int 型数组对象的引用,其中保存的值就是数组对象的首地址,为什么?
保存的就是数组的首元素的地址(非常重要!
)
因为数组是线性表、只需要保存首元素地址,就可以直到所有的元素地址,因为一旦找到首元素、之后的每个元素都是连续存储,如果是 int 型数组、之后的元素地址依次加 4 即可找到,就都能找到了~~
然后、通过这个引用指向相应地址的数组对象。
举一反三、画出如下代码的内存图:
public static void main( String[] args )
{
int [] arr = new int [] { 10, 20 };
swap ( arr );
}
public static void swap( int [] a )
{
a = new int [] { 10, 20 };
int temp = a[0];
a[0] = a[1];
a[1] = temp;
}
直接画:
总结:
1.数组对象就是在堆中保存的实体
,一个new
、对应在堆中开辟一个新的对象地址空间
。
2.数组的引用
就是给这块数组对象起了一个名
字,保存
这个数组对象的首地址
而已。
关于堆中对象占用的内存、什么时候释放?
Java中垃圾收集器自动地定期扫描Java对象的动态内存,并将所有的引用对象加上标记,在对象运行结束后(无引用变量对该对象进行关联),清除其标记,并将所有无标记的对象作为垃圾进行回收,释放垃圾对象所占的内存空间。
对象运行结束后或生命周期结束时,将成为垃圾对象,但并不意味着就立即会被回收,仅当垃圾收集器空闲或内存不足时,才会回收他们。
Java中每个对象都拥有一个finalize()方法:
垃圾回收器在回收对象时自动调用对象的finalize()方法来释放系统资源。
参考链接
2.数组的简单使用
(1)数组的常用方法
a.打印数组
创建一个方法、接收任意的整型数组并打印。
public static void printArr ( int [] a )
{
for ( int n : a )
System.out.println( n + " " );
}
b.数组元素扩大
实现一个方法transform,以数组为参数,将数组中的所有元素的值扩大为原来的两倍。
public static void transform ( int [] a )
{
for ( int i = 0; i < a.length; i ++ )
a[i] = a[i] * 2;
}
c.拷贝数组
实现一个方法copyOf,对一个整型数组进行拷贝,得到一个新的数组,并返回那个新的数组。
public static int[] copyOf ( int [] a )
{
int [] newArr = new int [a.length];
for ( int i = 0; i < a.length; i ++ )
{
newArr[i] = a[i];
}
return newArr;
}
这个方法的内存图:
(2)有关于数组的工具方法
Arrays.toString()
数组对象转为字符串对象
以后看到JDK中的某些类,在某些类后加s,这种类都是工具类,提供了大量有用的方法,直接调用
Arrays类
的toString方法
。
其中Arrays => 数组的工具类
:
包含了数组转字符串
的方法、数组排序
的方法、等等操作数组的各种方法都在这个类中。使用前需要导入import java.util.Arrays;
public static void main( String[] args)
{
int [] data = {1, 3, 5};
String str = Arrays.toString(data);
System.out.println(str);
}
结果:
Arrays.copyOf()
从一个原数组拷贝、得到一个新的数组;其中的方法参数第一个为原数组名称
、第二个参数为拷贝长度
。
int [] newArr
= Arrays.copyOf( arr, length );
有三种情况:
1.若新数组长度 < 原数组长度 部份拷贝
从原数组的第一个元素
开始拷贝,直到元素个数达到新数组的长度
即可;
2.若新数组长度 = 原数组长度 全部拷贝
3.若新数组的长度 > 原数组长度,也是全部拷贝,剩余的位置,用该数组数据类新的默认值
来补充。
int [] data = new int [] {1, 2, 3, 4, 5, 6};
int [] newData = Arrays.copyOf(data, 3);
System.out.println( Arrays.toString(newData) );
结果:
(3)数组的常用方法2
a.二分查找
在有序(升序)的集合上,使用二分查找。
在有序区间中查找一个元素toFind,我们不断比较待查找元素和区间中间位置元素的大小关系。
初始时、另 left = 0、right = arr.length - 1、则mid 一定是中间元素。
1.若toFind < arr[mid]
、说明这个元素一定在左区间
,一定小于arr[mid…right] 的所有元素。另right = mid - 1
(让区间右边界刚好比中间元素小一点,因为待查找元素小于中间元素,接下来必须在中间元素的左边查找)、继续比较。
2.若toFind == arr[mid] ,说明中间位置元素恰好就是待查找元素,直接返回mid
即可。
3.若toFind > arr[mid]
,则待查找元素大于左区间所有值。另left = mid + 1
(让区间左边界比中间元素大一点),继续比较。
循环的终止条件是什么:
分析:
arr[ left … right ]
1.left < right,这个区间中一定存在其他元素,且元素个数 >= 2
2.left = right,这个区间中还有一个元素,就一个啦~~
arr [ 6…6 ]、只剩一个元素
所以:
循环的终止条件就是left > right
left > right 时说明区间中一个元素都没有
,这个集合没有找到想要的元素。
代码:
public static int sort2sort( int [] arr , int toFind )
{
int left = 0;
int right = arr.length - 1;
while( left <= right )
//这是一个while循环、目的是为了实现循环
//才能不断比较、直到找到目标下标
{
int mid = (left + right) / 2;
这个为什么一定要写在while循环里?
因为只有写在while循环里、才会让每次运行之后的mid发生改变
(每次运行之后、left和right其中肯定会有一个发生变化、
这就会导致mid也跟着发生改变。)
如果把这句话写在while循环外,那么mid就不会变化,程序陷入死循环
if( toFind < arr[mid] )
{
right = mid - 1;
}
else if ( toFind > arr[mid] )
{
left = mid + 1;
}
else
// toFind == arr[mid]
{
return mid;
}
}
return -1;
// 没有找到
}
运行:
b.用递归写二分查找
从宏观思考问题
既然要用递归来写二分查找、那么就一定要让left和right在传入参数的时候就要被改变、为什么?因为用递归写,我们肯定是定义一个方法、它的语义是求一个数在数组中的什么位置
、那么我们就需要不断的改变边界、并不断的返回、直到遇到递归的终止条件,那么,此时显然,不断地改变边界是不可能由我们自己去面向过程的,必须由这个写好的方法、替我去实现
。
猜测方法的形参有:整型数组,待查找元素,可能会发生改变的左边界,以及可能会改变的右边界。大胆猜测public static int recursionBinarySearch ( int [] a, int toFind, int left, int right )
那么递归的终止条件是什么?
当待查找元素刚好等于此时的 a[mid] 时、循环跳出。
为什么强调此时
?因为边界在变
public static int binarySearch ( int [] a, int toFind , int left, int right )
{
int mid = (left + right) / 2;
if ( toFind == a[mid] )
return mid;
if ( toFind < a[mid] )
return binarySearch( a, toFind, left, mid - 1 );
if ( toFind > a[mid] )
return binarySearch( a, toFind, mid + 1, right );
return -1;
}
这个函数的传入参数很有讲究、我们甚至可以
自己定义边界
、也就是说、如果当我们在查找之前,确定了这个元素在数组中的大致位置之后,可以提高程序的效率。
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
int [] arr = new int [] {1, 3, 5, 7, 9, 100};
System.out.println("示例数组为:" + Arrays.toString( arr ));
System.out.println("输入你想要查找的元素:");
int num = s.nextInt();
int answer = binarySearch( arr, num, 0, arr.length-1 );
if( answer != -1 )
System.out.println("找到了!且下标为:" + answer );
else
System.out.println("没有找到、哈哈");
}
c.有序数组
判断一个数组是否是有序数组,默认升序,找反例
即可。
所谓的
升序数组
,就是前一个元素 > 后一个元素
若在遍历的过程中,发现有一个元素比后一个元素大,找到了一个反例。
只要有一个反例
,则这个数组一定不是升序数组。
1 2 3 4 5 68 7
9 =>false
public static boolean isSorted( int [] a )
{
for ( int i = 0; i < a.length; i ++ )
{
if( a[i] > a[i+1] )
{
//数组无序
return false;
}
}
//数组有序
return true;
}
d.数组最大值
给定一个整型数组,找出这个数组的最大值?
“
打擂
” 思想: 我们在寻找一个数组最大值的过程中,需要从数组第一个元素开始遍历,直到走到数组的最后一个元素,找出最大值。
所谓的大小,必须有一个待比较的元素
~~
在找一个集合的最大 / 最小值、
默认取数组第一个元素假定
为最大值或最小值,然后再依次
和数组其他元素进行比较
。
public static int max ( int [] a )
{
//从下标为 1 开始、即第二个元素
int maxElement = a[0];
for ( int i = 1; i < a.length; i ++ )
{
if ( a[i] >= maxElement )
maxElement = a[i];
}
return maxElement;
}
e.冒泡排序
默认排序都是升序
核心思想:假设现在数组有 n 个元素,每进行一次遍历,就将当前数组中
最大值放到数组末尾
每进行一次遍历,就有一个元素到达了最终位置。
将整个数组看作两个子数组
。初始时:待排序的数组 [ 0...n ]
、排好序的数组 []
每进行一次遍历,待排序的元素个数 -1,排好序的元素个数 + 1
内层循环就是每一次比较时,真正需要比较的次数
,每完成一次外层循环,就要把内层循环的循环次数减1。
外层循环:
for ( int i = 0; i < arr.length - 1 ; i ++ )
{
boolean isSwaped = false;
最外层循环表示
一共要遍历的次数
。
每进行一次遍历,就有一个元素到达了最终位置
。
有 n 个元素、就要比较 n 次
从下标为 0 到 n-1
、即i = 0 到 i = n-1 。
优化:
当待排序的数组
只剩下一个元素
时,整个数组已经有序
、就不需要再进行遍历了
所以遍历次数、可以再减一
(从 i = n-1 到 i = n-2 )
只需要比较n-1 次、即 i < n-1
i <= n-2 等价于 i < n-1 最后一次 i 可以取到的值都是 n-2
内层循环:
for ( int j = 0; j <= arr.length - 2 - i ; j++
)
j应该是进行n次、(包括和自身的比较)、所以是从0到len-1,即 j<= len-1
但是,为了保证 j + 1 不越界、应该再-1、是 j <= len - 2
为什么-i?
-i
是因为在每进行完一次内层的 for 循环
时(完成一个最大的元素到数组最后面
)
下次再进行 内层 for 循环时、待比较的元素就会减一
又外层循环表示的是总共要进行的比较的元素次数
、有 n 个元素、就比较 n 次
所以每成功进行一次循环,就让内层循环的循环次数-1、所以-i
{
if ( arr[j] > arr[j+1] )
{
//当进入内层循环、只要进行了一次比较,就令标志变量为真
isSwaped = true;
//若前一个大于后一个,则交换顺序
//交换三连
int temp = arr[j];
arr[j] = arr[ j + 1 ];
arr[ j + 1 ] = temp;
}
}
优化
if ( !isSwaped )
{
//如果在每次 外层 for 循环的 i 取一个值进入内层之后
//而内层循环没有进行一次交换时,提前停止这次内层循环
break;
}
}//外层 for 循环的大括号
f.数字排列问题
给定一个整型数组,将所有的偶数放在前半部分,将所有的奇数放在后半部分。
定义两个变量 a 和 b、一个从前往后找奇数,一个从后往前找偶数。如果 a 在数组前半部分找到奇数、并且
此时 b 在数组后半部分也找到了一个偶数、则另 a 与 b 交换三连,并终止这次查找。(只有在 b 也找到时才交换、否则正常结束这次循环,什么事也不做)
很明显,我们发现,外层循环的循环次数就是数组长度的一半,是有( arr.length - 1 ) / 2
个 a,每一个 a
都要与数组后半部分的所有的 b
进行比较,所以内层循环次数也是数组长度的一半。但是要注意边界问题,a不能大于b,但是也不是每一个外层循环的 a 都能完整的进行它的内层循环里的所有比较
、因为只要
在众多的 b 里找到一个
为偶数,就与a交换,并且,立即终止
这次内层循环。
public static void numArray( int [] a )
{
int left = 0;
int right = a.length-1;
int mid = (left + right) / 2;
for( int i = left; i <= mid; i ++ )
{
for ( int j = right; j > mid; j -- )
{
if( a[i] % 2 != 0 )
{
if( a[j] % 2 == 0 )
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
break;
}
}
}
}
}
双引用解法:
public static void num_of_array( int [] a )
{
int fir = 0;
int sec = 0;
while ( sec <= a.length-1 )
{
// 如果sec遇到偶数、就让fir和sec互换
// 如果fir是奇数、正好实现了交换
// 如果fir是偶数、偶数和偶数交换也没有影响
if( a[sec] % 2 == 0 )
{
int temp = a[fir];
a[fir] = a[sec];
a[sec] = temp;
fir++;
sec++;
}
// 如果sec没有找到偶数、继续向后找
else
{
sec++;
}
}
}