文章目录
1.什么是数组
什么是数据结构
2.数组的创建与初始化
数组的动态初始化
写法一:数据类型[ ] 数组名称 = new 数组类型[ ] {初始化数据} ; - 大括号可选
int [] arr = new int[] {1,3,5,7,9};
写法二:数据类型[ ] 数组名称 = new 数组类型[num]; - num表示当前数组的最大元素
int [] arr = new int[5];
两种写法的区别只不过是有没有初始化
在创建数组时,若没有使用{}来初始化每个元素的值,每个元素都是该数据类型的默认值
数组的静态初始化
数据类型[] 数据名称 = {初始化数据}
int[] arr = {1,2,3,4,5};
实际上,静态初始化就是Java的语法糖,也就是说,javac编译过后,就是动态初始化:
在IDEA中可以通过javac命令后,观察class文件可以看到:
int[] data = new int[n];
Java在定义数组的时候,可以是变量,因为变量在使用的时候是确定值,即使后面改变了他的值,但空间已经开辟好了,不会因为变量值的改变而改变
3.数组的使用
获取数组的长度
语法:数组名称.length
int[] arr1 = new int[] {1,3,5,7,9};
int[] arr2 = new int[5];
System.out.println(arr1.length);//执行结果 5
System.out.println(arr2.length);//执行结果 5
如何访问数组元素
使用:数组名称[要访问的元素相较于第一个元素的偏移量]
等同于:数组名称[元素的索引]
元素的索引从0开始计算,最后一个元素的索引arr.length - 1
int[] arr1 = new int[] {1,3,5,7,9};
int[] arr2 = new int[5];
System.out.println(arr1[0]);//执行结果 1
System.out.println(arr1[4]);//执行结果 9
System.out.println(arr1[6]);//报错:数组异常访问,即越界
什么是索引,为什么从0开始?
索引起始就是“偏移量”,相较于数组的第一个元素的单位长度
存储数据 | 1 | 3 | 5 | 7 | 9 |
---|---|---|---|---|---|
索引/偏移量 | [0] | [1] | [2] | [3] | [4] |
数组在内存中存储时,每个元素之间都是顺序存储的
保存的就是数组的首元素的地址,要找到其他元素,只要知道其他元素相较于第一个元素的距离就行
遍历数组
int[] arr1 = new int[] {1,3,5,7,9};
//访问数组arr1的每个元素
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + " ");
}
System.out.println();
//JDK1.5引入的for-each循环,增强型for循环
for(int i : arr1){
System.out.print(i + " ");
}
//两者的执行结果完全一致
但是两种循环里的i代表着不同的意思:
第一种循环里的i表示数组中每个元素的索引下标
第二种循环里的i指的是从数组的第一个元素开始取取值,第一次把第一个元素的值拷贝一份给i,第二次循环把第二个元素的值拷贝一份给i,依次类推,直到整个数组都遍历结束
第二种循环只能读取数组的元素值,无法修改,i是原数组每个元素的值拷贝,并不是实实在在的数组元素
而第一种里的arr[i]确实拿到了每个数组的元素
for循环:
for (int i = 0; i < arr1.length; i++) {
if( i == 2){
arr1[i] = 55;
}
}
System.out.println("第三个元素的值为:" + arr1[2]);
//执行结果为 55
增强for-each循环:
for(int i : arr1){
if(i == 5){
i = 55;//尝试把数组中值为5的元素改为55
}
}
System.out.println("第三个元素的值为:" + arr1[2]);
//执行结果为 5
//也就是说arr1中的内容并没有别修改
4.数组和方法之间的关系
数组是引用数据类型,比如int[]是对整型数组的引用
数组作为方法的参数
创建一个方法,接收任意的整型数组并打印:
public static void printArr(int[] num){
for(int i : num){
System.out.print(i + " ");
}
}
//int[] num 是形参
交换两个整型:
public static void main(String[] args) {
int x = 10;
int y = 20;
swap(x, y);
System.out.println("x = " + x + ", y = " + y);
}
public static void swap(int a , int b){
int tmp = a;
a = b;
b = tmp;
}
//执行结果 a = 10, b = 20
如果创建两个变量,然后利用方法交换变量时,实际上是把位于主方法中的临时变量,即实参传递给交换方法的,方法接收的不过是实参的临时拷贝,即形参
理解引用类型
JVM把内存划分为6个区域,今天只重点讲“栈区”和“堆区”
方法的调用就是在栈区进行的,每个方法的调用过程,就是一个栈帧的入栈和出栈的过程
“栈” - 先进后出的结构,LIFO
堆 - 看待new关键字,new出来的对象都在堆中保存
方法中的局部变量和形参都在栈中存储,当方法调用结束出栈时,临时变量都会销毁
利用数组交换
public static void main(String[] args) {
int[] arr = {10, 20};
swapArr(arr);
System.out.println("arr[0] = " + arr[0] + ", arr[1] = " + arr[1]);
}
public static void swapArr(int[] arr){
int temp = arr[0];
arr[0] = arr[1];
arr[1] = temp;
}
//执行结果 arr[0] = 20, arr[1] = 10
//可见确实是被修改了
JVM的另一块内存区域称为堆区,堆区存放了所有对象,这个区域是所有线程共享的
包括:数组对象,类的实例化对象,接口的对象
什么是引用:
int[] arr = new int[] {10, 20};
后半部分,用new出来的是对象,这里就是数组对象,在堆上存储
前半部分int[] arr
就是数组引用
类比生活:
回到这个代码:
int[] arr = new int[] {10, 20};
程序的执行都是从右向左,执行的时候程序先看到new,会在堆中创建这个数组对象
数组对象在堆中也是实实在在存在的,通过交换方法中的int[] arr里存的地址,修改了堆中数组对象的值,这个修改对于主方法中的arr是可见的
本质上,两个引用都指向了堆中同一块区域
我们修改一下swap方法:
public static void main(String[] args) {
int[] arr = {10, 20};
swapArr(arr);
System.out.println("arr[0] = " + arr[0] + ", arr[1] = " + arr[1]);
}
public static void swapArr(int[] arr){
arr = new int[] {10, 20};//在java中,看见new关键字,就一定在堆中开辟了新的空间
int temp = arr[0];
arr[0] = arr[1];
arr[1] = temp;
}
//执行结果 arr[0] = 10, arr[1] = 20
//可见arr中的值并没有被修改
刚进入swap时的内存图:
执行了swap中第一条语句之后的内存图:
调用完swap之后的内存图:
关于会否会内存泄露的问题:
什么时候会释放?
由于swap出栈之后,没有任何变量指向0x200,如果想要调出swap中的arr,只能在main中创建新的数组变量来接收swap中的arr中存储的首元素地址
public static void main(String[] args) {
int[] arr = {10, 20};
int[] ret = swapArr(arr);
System.out.println("arr[0] = " + arr[0] + ", arr[1] = " + arr[1]);
System.out.println("ret[0] = " + ret[0] + ", ret[1] = " + ret[1]);
}
public static int[] swapArr(int[] arr){
arr = new int[] {10, 20};
int temp = arr[0];
arr[0] = arr[1];
arr[1] = temp;
return arr;
}
//执行结果
//arr[0] = 10, arr[1] = 20
//ret[0] = 20, ret[1] = 10
这里在swap在结束之前,把arr的值返回给main,这样,swap中的arr指向堆中的空间的地址就会被保存下来,在main中,就可以继续通过swap的返回值来访问堆中的空间
5.数组练习
在JDK中的某些类后面加s的,这种类都是工具类,提供了大量有用的方法,可以直接调用
Arrays - 数组的工具类,包含数组转字符串的方法,数组排序的方法,等等操作数组的各种方法都在这个类中,我们直接通过类名称来调用
Collections - 集合的工具类
public static void main(String[] args) {
int[] arr = {1,3,5};
String str = Arrays.toString(arr);
System.out.println(str);
}
//执行结果 [1, 3, 5]
数组对象转为字符串对象
模拟实现Arrays.toString
public static void main(String[] args) {
int[] arr = {1,3,5};
String str = arrToString(arr);
System.out.println(str);
}
public static String arrToString(int[] arr){
String ret = "[";
for (int i = 0; i < arr.length; i++) {
ret += arr[i];
//如果是数组中的最后一个元素,不需要再加分隔符
if(i != arr.length - 1) {
ret += ", ";
}
}
ret += "]";
return ret;
}
//执行结果 [1, 3, 5]
拷贝数组
Arrays.copyOf(data, length)
data - 是原数组,length - 是需要拷贝的新数组的长度
几种情况:
模拟实现 Arrays.copyOf
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8};
int[] ret = arrayCopyOf(arr, 10);
System.out.println(Arrays.toString(ret));
}
public static int[] arrayCopyOf(int[] arr, int length) {
int[] ret = new int[length];
if(length <= arr.length){
for (int i = 0; i < length; i++) {
ret[i] = arr[i];
}
}else{
for (int i = 0; i < arr.length; i++) {
ret[i] = arr[i];
}
for (int i = arr.length; i < length; i++) {
ret[i] = 0;
}
}
return ret;
}
Arrays.copyOfRange(data, start, over)
- 数组区间拷贝
从下标开始位置拷贝,直到结束为止,区间是左闭右开的,即 [ strat, over )
[就是从下标为start的地方一直到下标over,但不拷贝下标为over的值
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8};
int[] ret = Arrays.copyOfRange(arr, 1, 4);
System.out.println(Arrays.toString(ret));
}
//执行结果 [2, 3, 4]
找整数数组最大值
给定一个整数数组,找出这个数组的最大值 - “打擂”思想
找到一个集合的最大/最小值,默认取第一个数组元素,并假定其为最大/最小值,但不能取一个随机的数组
比如int max = 0;
这是不可取的,如果数组全是负数,那么0就是最大值,但0不是数组元素
public static void main(String[] args) {
int[] arr = {1,5,3,5,7,8,9,2,4,6};
int max = arrMax(arr);
System.out.println("max = arr[" + max + "] = " + arr[max]);
}
public static int arrMax(int[] arr){
int max = 0;
for (int i = 0; i < arr.length; i++) {
//如果有元素大于max,则赋值给max,否则什么都不做
if(arr[max] < arr[i]){
max = i;
}
}
return max;
}
//执行结果:
//max = arr[6] = 9
求数组的平均值
public static void main(String[] args) {
int[] arr = {1,3,5,7,9};
System.out.println("avg = " + arrAvg(arr));
}
public static double arrAvg(int[] arr){
double sum = 0;
for (int i : arr) {
sum += i;
}
return sum / arr.length;
}
//执行结果 5
查找指定元素
查找一个数组中是否包含指定元素,若存在,返回该元素的索引,若不存在,返回-1
如果数组中存在多个相同的元素,默认返回第一个
public static void main(String[] args) {
int[] arr = {1,5,3,5,7,8,9,2,4,6};
System.out.println(arrFind(arr, 7));
}
public static int arrFind(int[] arr, int key){
for (int i = 0; i < arr.length; i++) {
//判断key是否和数组元素相等,相等则返回下标
if(key == arr[i]){
System.out.print("找到了,下标是");
return i;
}
}
//没有找到对应的元素,返回 -1
return -1;
}
//执行结果:
//找到了,下标是4
二分查找
前提:在有序(升序或降序)的集合上,才能使用二分查找
在有序区间中查找一个元素key,我们不断比较待查找元素和区间中间位置元素的大小关系
若key < arr[mid],则说明这个元素一定左区间,一定小于arr[mid, right]的所有元素
right = mid - 1开始继续判断
若key == arr[mid],说明中间位置元素恰好就是待查找元素,直接返回mid即可
若key > arr[mid],则待查找元素大于左区间所有值,left = mid + 1继续在右区间中查找元素
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
System.out.println(binarySearch(arr, 8));
}
public static int binarySearch(int[] arr, int key){
int left = 0;
int right = arr.length - 1;
while(left <= right){
int mid = (left + right) / 2;//mid每次循环都会变
if(key < arr[mid]){
right = mid - 1;
}else if(key > arr[mid]){
left = mid + 1;
}else{
System.out.print("找到了,下标是");
return mid;
}
}
return -1;
}
//找到了,下标是7
当一个有序集合的长度为n时,顺序查找法,比较的次数为n
二分查找则是经过 n/2/2/2/2/2… == 1后找到,即log2 n
递归实现二分查找:
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
System.out.println(binarySearch(arr, 1, 0, arr.length - 1));
}
public static int binarySearch(int[] arr, int key, int left, int right){
if(left > right){
//找不到,直接返回
return -1;
}
int mid = (left + right) / 2;
if(key < arr[mid]){
//key在左区间
return binarySearch(arr, key, left, mid - 1);
}
if(key > arr[mid]){
//key在右区间
return binarySearch(arr, key, mid + 1, right);
}
//如果上面都不满足,说明找到了
System.out.print("找到了,下标是");
return mid;
}
//执行结果:找到了,下标是0
判断数组是否有序
以升序为例,如果有序,那么前一个元素 <= 后一个元素,否则就不是有序数组
public static void main(String[] args) {
int[] arr = {1,3,4,5,6,2,9};
System.out.println(isSorted(arr));
}
public static boolean isSorted(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
if(arr[i] > arr[i + 1]){
System.out.println("不是有序数组");
return false;
}
}
System.out.println("是有序数组");
return true;
}
//执行结果:
//不是有序数组
//false
写循环的边界条件时,最大的取值不能越界,尤其是类似于 i + 1 这样的下标,必须保证 i + 1不能越界
冒泡排序
一般排序都排升序
核心思想:假设现在数组有n个元素,每进行一次遍历过程,就将当前数组中最大值放在数组末尾
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j+1] = tmp;
}
}
}
}
// 执行结果
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
冒泡优化:
如果内存没有进行交换,则说明,该数组已经有序,无需再进行排序
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
boolean isSorted = true;
for (int j = 0; j < arr.length - i - 1; j++) {
if(arr[j] > arr[j+1]){
isSorted = false;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j+1] = tmp;
}
}
//如果遍历一遍,没有发生交换,数组必然有序,因此没必要再排序
if(isSorted){
return;
}
}
}
数组逆序
public static void main(String[] args) {
int[] arr = {1,2,3,4};
int[] arr1 = {1,2,3,4,5};
reverse(arr);
reverse(arr1);
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr1));
}
public static void reverse(int[] arr){
int left = 0;
int right = arr.length - 1;
while(left < right){
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
//[4, 3, 2, 1]
//[5, 4, 3, 2, 1]
while循环和for循环的区别:
数组的数字排列问题
给定一个整型数组,将所有的偶数放在前半部分,将所有的奇数放在数组后半部分
思考方法:
从前向后找到第一个奇数停止,从后向前找到第一个偶数停止,交换
然后继续,知道left >= right
不过我们要考虑到全是奇数或者偶数的可能
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
swap(arr);
System.out.println(Arrays.toString(arr));
}
public static void swap(int[] arr){
int left = 0;
int right = arr.length - 1;
while(left < right){
//如果全是奇数或者偶数,内层循环坑会越界,因此也要判断left < right
while(left < right && arr[left] % 2 == 0){
left++;
}
//如果right左边有奇数,left一定停留在从左到右的第一个奇数,否则一直执行到left==right
while(left < right && arr[right] % 2 == 1){
right--;
}
//如果left右边有偶数,right一定停留在从右到左的第一个偶数,否则一直执行到left==right
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
//执行结果 [6, 2, 4, 3, 5, 1]
6.二维数组
所谓的二维数组就是多了列的概念
语法:
public static void main(String[] args) {
int[][] arr = new int[][] {
{1,2,3},
{4,5,6},
{7,8,9}
};//内层的大括号表示一行
for(int row = 0;row < arr.length;row++){
for(int col = 0;col < arr[row].length;col++){
System.out.print(arr[row][col] + " ");
}
System.out.println();
}
}
//执行结果
1 2 3
4 5 6
7 8 9
实际上,二维数组里的每一行就是个一维数组,二维数组里存储的是一维数组的地址