Java基础---数据、查找、排序

1 为什么需要数组

1.1 数组介绍

  • 定义:数组是一种基本的数据结构,用于存储固定大小相同类型的元素序列。
  • 特点
    • 类型固定:数组中的所有元素都必须是相同类型的。
    • 连续内存:数组元素在内存中是连续存储的,这使得访问速度快。
    • 索引访问:可以通过索引快速访问任何一个元素。
    • 固定大小:数组的大小在声明时确定,之后不能改变(单维数组)。
  • 用途
    • 存储和管理大量相同类型的数据。
    • 实现算法,如排序、搜索等。
    • 作为函数参数传递数据集合。

1.2 数组快速入门

  • 声明数组

    • int[] myArray; 声明一个整型数组。
    • double[] prices = new double[10]; 声明并初始化一个大小为10的双精度浮点型数组。
  • 初始化数组

    • 静态初始化:在声明时初始化数组。

      int[] myArray = {1, 2, 3, 4, 5};
      
    • 动态初始化:先声明数组,然后使用new关键字动态分配大小。

      int[] myArray = new int[5]; // 分配一个大小为5的整型数组
      
  • 访问数组元素

    • 使用索引访问数组元素,索引从0开始。

      int firstElement = myArray[0]; // 获取第一个元素
      
  • 遍历数组

    • 使用for循环遍历数组中的每个元素。

      for (int i = 0; i < myArray.length; i++) {
          System.out.println(myArray[i]);
      }
      
  • 数组的长度

    • .length属性可以用来获取数组的长度,即数组中元素的数量。

​ 这个部分提供了数组的基本概念和如何快速开始使用数组。

当然,以下是使用Markdown语法整理的关于Java数组使用的知识点。

2 数组的使用

2.1 动态初始化

动态初始化是指在创建数组时不立即指定数组的元素,而是在创建数组对象后,再为数组分配内存空间。

int[] dynamicArray;
// 声明数组但不立即分配内存

// 稍后分配内存
dynamicArray = new int[10];
// 此时数组的大小为10,但元素默认值为0

在Java中,数组的元素在声明时如果没有显式初始化,则会被自动初始化为默认值。对于整型数组,元素的默认值是0

2.2 静态初始化

静态初始化是在声明数组的同时,直接指定数组的元素。

int[] staticArray = {10, 20, 30, 40, 50};
// 声明并初始化数组,数组的大小为5,元素值分别是10, 20, 30, 40, 50

静态初始化允许你定义数组时立即设置数组的元素,这在你需要一个具有特定初始值的数组时非常有用。

2.3 多维数组的初始化

后面会讲到多为数组。

int[][] twoDimensionalArray = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
// 声明并初始化一个3x3的二维数组

多维数组可以视为数组的数组,通常用于表示矩阵或需要多个维度的数据结构。

3 数组使用注意事项和细节

在使用Java数组时,了解一些注意事项和细节可以帮助你避免常见的错误,并提高代码的效率和可读性。

3.1 避免数组越界

3.1.1错误示例

尝试访问数组的索引超出其长度,将导致ArrayIndexOutOfBoundsException

int[] array = new int[5];
System.out.println(array[5]); // 抛出异常,因为索引5超出了数组的范围

3.1.2错误原因

在Java中,数组索引是从0开始的,所以一个长度为5的数组,其有效索引范围是从0到4。尝试访问索引5的元素会导致ArrayIndexOutOfBoundsException,因为索引5超出了数组的有效范围。

3.1.3异常详情

  • 异常类型ArrayIndexOutOfBoundsException
  • 异常信息:通常包含“Index x out of bounds for length y”的格式,其中x是尝试访问的索引,y是数组的长度。

3.1.4调试和解决

  1. 检查索引值:确保索引值在数组的有效范围内。
  2. 使用循环:使用循环遍历数组时,确保循环条件不会导致越界。
  3. 添加边界检查:在访问数组元素之前,添加检查以确保索引有效。

3.1.5最佳实践:

在访问数组元素之前,始终检查索引是否在有效范围内。

if (index >= 0 && index < array.length) {
    System.out.println(array[index]);
}

3.2 理解数组的默认值

  • 细节:当数组被声明并分配内存后,如果没有显式初始化,其元素将被赋予默认值。
  • 示例:对于整型数组,默认值是0;对于浮点型数组,默认值是0.0

3.3 避免使用魔法数字

3.3.1什么是魔法数字

在编程中,“魔法数字”(Magic Number)指的是在代码中直接硬编码的数值,这些数值没有明确的名称或含义,它们被直接嵌入到代码中。魔法数字使得代码难以理解和维护,因为它们的含义不是立即显而易见的。

3.3.2错误示例

在数组操作中直接使用硬编码的数字,这降低了代码的可读性和可维护性。

int[] array = new int[10]; // 魔法数字10

魔法数字的问题

  • 难以理解:新阅读代码的开发者可能不知道数字10的来源和目的。

  • 难以维护:如果需要更改数组的大小,可能需要在代码的多个地方找到并替换这个数字。

  • 代码重复:如果相同的数字在代码中多次出现,每次更改都需要在多个地方进行更新。

  • 缺乏灵活性:硬编码的数字限制了代码的灵活性和可重用性。

3.3.3最佳实践

使用常量变量代替硬编码的数字。

final int ARRAY_SIZE = 10;
int[] array = new int[ARRAY_SIZE];

3.4 注意数组的不可变性

数组在内存中是连续存储的,其大小在声明时就已经确定。因为数组元素的内存地址是连续的,改变数组的大小将需要重新分配内存并复制所有元素到新的位置,这在语言层面上是不提供的。

3.4.1解决和代替方案

  1. 使用动态数组:Java提供了ArrayList这样的动态数组实现,它可以根据需要自动调整大小。

    ArrayList<Integer> list = new ArrayList<>();
    list.add(1); // 添加元素
    
  2. 复制数组:如果需要“扩展”数组,可以创建一个新的数组,并将原始数组的内容复制到新数组中。

    int[] original = new int[]{1, 2, 3};
    int[] expanded = new int[]{original.length + 1};
    System.arraycopy(original, 0, expanded, 0, original.length);
    
  3. 使用Arrays.copyOf:Java提供了Arrays.copyOf方法来复制数组并扩展大小。

    int[] newArray = Arrays.copyOf(original, original.length + 1);
    
  4. 使用System.arraycopy:可以手动复制数组内容到新的数组中。

    int[] newArray = new int[original.length + 1];
    System.arraycopy(original, 0, newArray, 0, original.length);
    
  5. 使用循环:在某些情况下,可以使用循环来手动复制数组元素。

    复制int[] newArray = new int[original.length + 1];
    for (int i = 0; i < original.length; i++) {
        newArray[i] = original[i];
    }
    

3.4.2注意事项

  • 性能考虑:复制数组是一个昂贵的操作,因为它涉及到创建新的内存空间和复制元素。在性能敏感的应用中,应该谨慎使用。
  • 内存管理:在复制数组时,原始数组仍然存在于内存中,直到被垃圾回收器回收。确保不再使用原始数组,以避免内存泄漏。
  • 数据一致性:在复制数组时,要确保所有相关数据保持一致性,特别是在多线程环境中

3.5 复制数组元素时要小心

在Java中,当你将一个数组赋值给另一个数组变量时,它们会指向内存中的同一数组对象。这意味着对一个数组所做的任何更改都会反映在另一个数组上,因为它们是同一个数组的两个引用。

3.5.1浅拷贝示例

int[] array1 = {1, 2, 3};
int[] array2 = array1; // 浅拷贝,array2是array1的别名

array2[0] = 4; // 修改array2也会影响array1
System.out.println(array1[0]); // 输出4,array1也被修改了

3.5.2如何进行深拷贝

要创建数组的一个完全独立的副本,需要手动复制数组中的每个元素。

int[] array1 = {1, 2, 3};
int[] array2 = new int[array1.length]; // 创建新的数组
for (int i = 0; i < array1.length; i++) {
    array2[i] = array1[i]; // 手动复制每个元素
}

array2[0] = 4; // 这次修改array2不会影响array1
System.out.println(array1[0]); // 输出1,array1保持不变

3.5.2使用Arrays.copyOf简化深拷贝

Java提供了Arrays.copyOf方法来简化数组的深拷贝过程。

int[] array1 = {1, 2, 3};
int[] array2 = Arrays.copyOf(array1, array1.length);

array2[0] = 4; // 修改array2不会影响array1
System.out.println(array1[0]); // 输出1,array1保持不变

3.5.3注意事项

  • 避免共享:如果你不希望修改一个数组时影响另一个数组,确保进行深拷贝。
  • 使用工具:利用Arrays.copyOfSystem.arraycopy等工具方法来帮助深拷贝。

通过这些简单的步骤,你可以避免在数组操作中出现的共享状态问题,确保你的数组是完全独立的副本。

3.6 考虑使用增强的for循环遍历数组

在Java中,遍历数组的传统方法是使用标准的for循环,通过索引来访问每个元素。Java 5引入了一种新的for循环语法,称为增强的for循环(也称为for-each循环),它提供了一种更简洁、更易于阅读的方式来遍历数组和集合。

3.6.1传统for循环示例

int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}

在这个例子中,我们使用一个索引变量i来访问数组的每个元素。

3.6.2增强的for循环示例

int[] array = {1, 2, 3, 4, 5};
for (int element : array) {
    System.out.println(element);
}

在这个例子中,增强的for循环自动遍历数组中的每个元素,变量element代表当前遍历到的数组元素。

3.6.3增强的for循环的优点

  1. 代码简洁:无需索引变量,代码更加简洁易读。
  2. 减少错误:避免了在使用索引时可能出现的越界或错误。
  3. 易于维护:当需要遍历数组或集合时,增强的for循环提供了一种统一的语法。

3.6.4注意事项

  • 不能使用索引:增强的for循环不提供对索引的访问,如果你需要索引,仍然需要使用传统的for循环。
  • 数组元素赋值:在增强的for循环中,不能对数组的元素进行赋值操作,因为循环中的变量element是只读的。
  • 修改数组:如果你需要在循环中修改数组(例如,添加或删除元素),则应使用其他数据结构,如ArrayList

3.6.5案例分析

假设我们有一个整数数组,我们想打印出所有的元素。

int[] numbers = {10, 20, 30, 40, 50};

// 传统for循环遍历
System.out.println("使用传统for循环:");
for (int i = 0; i < numbers.length; i++) {
    System.out.println(numbers[i]);
}

// 增强的for循环遍历
System.out.println("使用增强的for循环:");
for (int number : numbers) {
    System.out.println(number);
}

在这个案例中,我们展示了如何使用两种不同的循环方法来遍历同一个数组。增强的for循环提供了一种更简洁、更易于阅读的方式来完成相同的任务。

通过使用增强的for循环,你可以编写出更简洁、更易于维护的代码,同时保持了代码的可读性和功能性.

3.7 避免使用null代替空数组

在Java中,将数组变量设置为null是一种常见的做法,尤其是在不确定数组是否会被使用的情况下。然而,这可能会导致NullPointerException,因为调用数组的方法(如访问其长度或元素)需要数组引用指向一个实际的对象,而不是null

3.7.1错误示例

使用null导致的NullPointerException

int[] array = null; // 将数组设置为null
System.out.println(array.length); // 抛出NullPointerException

在这个例子中,尝试访问null引用的length属性会导致NullPointerException

3.7.2正确示例

使用空数组

int[] array = new int[0]; // 创建一个空数组,而不是null
System.out.println(array.length); // 输出0,安全且有效

在这个例子中,我们创建了一个空数组,它没有元素,但length属性是有效的,为0

3.7.3案例分析

假设你正在开发一个函数,该函数接受一个数组作为参数,并打印数组的长度。如果函数接收到null数组,它将抛出异常。

public class ArrayExample {
    public static void printArrayLength(int[] array) {
        if (array == null) {
            System.out.println("数组是null,没有长度。");
        } else {
            System.out.println("数组长度为: " + array.length);
        }
    }

    public static void main(String[] args) {
        int[] nullArray = null;
        int[] emptyArray = new int[0];

        printArrayLength(nullArray); // 输出: 数组是null,没有长度。
        printArrayLength(emptyArray); // 输出: 数组长度为: 0
    }
}

在这个案例中,printArrayLength函数首先检查传入的数组是否为null。如果是,它打印一条消息说明数组是null;如果不是,它打印数组的长度。这种方式可以防止NullPointerException的发生。

3.7.4最佳实践

  • 避免null:尽可能使用空数组代替null
  • 空检查:在操作数组之前,始终检查数组是否为null
  • 提供默认值:在函数或方法中,如果数组可能为null,提供一个合理的默认行为或值。

使用这些最佳实践可以帮助你避免在使用数组时遇到的NullPointerException问题。

3.8正确处理数组作为方法参数

当数组作为参数传递给方法时,由于Java的传值调用特性,实际上是将数组的引用传递给方法。这意味着方法内部对数组所做的任何修改都会反映到原始数组中。了解这一点对于正确处理数组参数至关重要。

3.8.1基本示例

public class ArrayMethodExample {
    public static void modifyArray(int[] array) {
        for (int i = 0; i < array.length; i++) {
            array[i] *= 2; // 将数组中的每个元素翻倍
        }
    }

    public static void main(String[] args) {
        int[] originalArray = {1, 2, 3, 4, 5};
        System.out.println("Original array: " + java.util.Arrays.toString(originalArray));
        
        modifyArray(originalArray);
        
        System.out.println("Modified array: " + java.util.Arrays.toString(originalArray));
    }
}

在这个例子中,modifyArray方法接收一个数组作为参数,并修改了数组中的每个元素。由于数组是通过引用传递的,originalArray在方法调用后也发生了变化。

3.8.2注意事项

  1. 引用传递:记住数组是引用传递的,任何在方法内对数组所做的修改都会影响原始数组。

  2. 避免不必要的修改:如果你不想修改原始数组,应该在方法内部使用数组的副本。

  3. 传递副本:如果需要在方法中修改数组,但不想影响原始数组,可以在方法调用之前创建数组的副本。

    int[] copy = java.util.Arrays.copyOf(originalArray, originalArray.length);
    modifyArray(copy);
    
  4. 返回新数组:如果方法需要返回一个新数组,可以创建一个新的数组实例并返回。

  5. 多维数组:多维数组的传递也遵循相同的规则,每个维度的数组都是通过引用传递的。

  6. 方法签名:在设计方法时,明确指出方法是否会修改传入的数组,这有助于调用者正确使用方法。

  7. 文档和注释:在方法的文档注释中说明数组参数的使用方式,包括是否会被修改。

  8. 异常处理:如果方法可能会抛出与数组相关的异常(例如访问越界),应该在文档注释中明确指出。

通过正确处理数组作为方法参数,你可以避免潜在的bug,并确保代码的可维护性和清晰性。

4数组复制机制详解

数组复制是Java编程中一个重要的概念,它涉及到创建数组的一个副本,使得原始数组和副本数组相互独立。这在很多情况下非常有用,比如当你需要保留原始数据不变,同时对数据的一个副本进行操作时。

4.1系统方法复制

Java提供了一些内置的方法来帮助复制数组:

4.1.1System.arraycopy()

  • 这是一个非常底层的数组复制方法,它允许你复制数组的一部分或全部到同一个数组或另一个数组中。
int[] original = {1, 2, 3, 4, 5};
int[] copy = new int[original.length];
System.arraycopy(original, 0, copy, 0, original.length);

4.1.2Arrays.copyOf()

  • 这个方法是System.arraycopy()的简化版本,它提供了一个更简洁的语法来复制数组。
int[] original = {1, 2, 3, 4, 5};
int[] copy = Arrays.copyOf(original, original.length);

4.1.3Arrays.copyOfRange()

  • 如果你只需要复制数组的一部分,可以使用这个方法。
int[] original = {1, 2, 3, 4, 5};
int[] partialCopy = Arrays.copyOfRange(original, 1, 3);
// partialCopy将会包含 {2, 3}

4.2自定义复制

除了使用系统方法,你也可以通过遍历数组来手动复制:

int[] original = {1, 2, 3, 4, 5};
int[] copy = new int[original.length];
for (int i = 0; i < original.length; i++) {
    copy[i] = original[i];
}

4.3注意事项

  • 深拷贝与浅拷贝:对于对象数组,需要考虑深拷贝和浅拷贝的问题。System.arraycopy()Arrays.copyOf()都是浅拷贝,它们只复制对象的引用,而不复制对象本身。

  • 多维数组:复制多维数组时,需要递归地复制每个子数组。

  • 性能:复制大型数组可能会消耗较多时间和资源,因此在性能敏感的应用中,需要谨慎使用。

  • 异常处理:在使用System.arraycopy()时,需要手动处理索引和范围,以避免IndexOutOfBoundsException

  • 不可变数组:一旦数组被创建,其长度就不可变。因此,如果需要调整大小,必须创建一个新的数组。

通过理解数组复制机制,你可以在需要的时候创建数组的独立副本,以保证原始数据的安全性和完整性。

5 数组反转

数组反转是将数组中的元素顺序颠倒的操作。例如,数组[1, 2, 3, 4, 5]反转后变为[5, 4, 3, 2, 1]。这是一个常见的操作,通常用于实现某些算法或数据结构。

5.1反转方法

使用Collections.reverse():

如果你有一个对象数组,可以使用java.util.Collections.reverse()方法来反转。

Integer[] array = {1, 2, 3, 4, 5};
Collections.reverse(Arrays.asList(array));
// 注意:这会改变原始数组array的顺序

双指针交换法:

使用两个指针,一个在数组的开始,一个在数组的末尾,交替地交换元素,然后向中间移动。

int[] array = {1, 2, 3, 4, 5};
int left = 0;
int right = array.length - 1;
while (left < right) {
    // 交换左右指针所指向的元素
    int temp = array[left];
    array[left] = array[right];
    array[right] = temp;
    // 向中间移动
    left++;
    right--;
}

递归方法:

也可以使用递归的方式来反转数组,通过先反转数组的前半部分和后半部分,然后再合并。

void reverseArray(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    int temp = array[start];
    array[start] = array[end];
    array[end] = temp;
    reverseArray(array, start + 1, end - 1);
}

循环交换法:

从数组的第一个元素开始,与最后一个元素交换,然后是第二个元素与倒数第二个元素交换,依此类推。

int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < array.length / 2; i++) {
    int temp = array[i];
    array[i] = array[array.length - 1 - i];
    array[array.length - 1 - i] = temp;
}

注意事项

  • 原地反转: 大多数反转方法都是原地操作,意味着它们直接在原始数组上进行修改。

  • 稳定性: 反转操作不会改变元素之间的相对顺序,除了方向相反。

  • 多维数组: 对于多维数组,你可能需要对每一行或每一列单独进行反转。

  • 性能: 反转操作的时间复杂度为O(n/2),即线性时间,其中n是数组的长度。

  • 边界条件: 在双指针交换法中,需要确保左指针不大于右指针。

  • 空或单元素数组: 对于空数组或只有一个元素的数组,反转操作不会执行任何操作。

通过使用数组反转技术,你可以在需要时轻松地对数组元素的顺序进行调整。

6 数组添加

在Java中,数组的大小在初始化后是固定的,这意味着你不能直接在原有数组的基础上添加更多的元素。但是,有几种方法可以间接实现在数组中添加元素:

方法1:使用ArrayList

ArrayList是一个动态数组,可以根据需要自动调整大小。它是java.util包的一部分,提供了灵活的方式来添加元素。

ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 添加元素1
list.add(2); // 添加元素2
// ...

方法2:创建新数组

如果你正在处理一个固定大小的数组,并且想要添加更多的元素,你可以创建一个新的数组,将原始数组的内容复制到新数组中,并添加新元素。

int[] originalArray = {1, 2, 3};
int[] newArray = new int[originalArray.length + 1];
System.arraycopy(originalArray, 0, newArray, 0, originalArray.length);
newArray[originalArray.length] = 4; // 在新位置添加元素4

方法3:使用Arrays.copyOf()

Arrays.copyOf()方法可以用来创建数组的副本,并在复制时指定新数组的大小。

int[] originalArray = {1, 2, 3};
int[] newArray = Arrays.copyOf(originalArray, originalArray.length + 1);
newArray[originalArray.length] = 4; // 添加新元素4

方法4:使用循环

如果你需要在数组的特定位置添加元素,可以使用循环来实现。

int[] array = {1, 2, 3};
int[] newArray = new int[array.length + 1];
for (int i = 0; i < array.length; i++) {
    newArray[i] = array[i]; // 复制原始数组的元素
}
newArray[array.length] = 4; // 在新位置添加元素4

注意事项

  • 性能考虑:每次添加元素时创建新数组和复制元素可能会影响性能,特别是在频繁操作时。

  • 内存管理:创建新数组时,原始数组仍然存在于内存中,直到垃圾回收器回收它。

  • 原始数组不变性:上述方法都不会修改原始数组,它们创建了数组的一个新副本。

  • 多维数组:对于多维数组,添加元素可能更加复杂,可能需要递归地处理每个子数组。

  • 边界情况:在添加元素时,要确保考虑所有边界情况,如添加到空数组中。

  • 使用合适的数据结构:如果经常需要添加元素,考虑使用ArrayList或其他动态数据结构,而不是固定大小的数组。

通过上述方法,你可以在Java中实现数组添加元素的功能,即使数组本身不允许动态扩展。

7 排序的介绍

排序算法是计算机科学中的一个基础概念,用于将一系列元素按照特定的顺序重新排列。

7.1 内部排序

内部排序算法是指在排序过程中,所有的数据都存储在内存中的排序方法。这类算法适合于数据量不是特别大,能够一次性装入内存的数据集。

内部排序算法的例子包括:

  • 冒泡排序:通过重复遍历待排序的序列,比较每对相邻元素的大小,并在必要时交换它们的位置。
  • 选择排序:在未排序序列中找到最小(或最大)元素,放到已排序序列的末尾。
  • 插入排序:构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
  • 快速排序:选择一个基准值,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小。
  • 归并排序:将序列分为两部分,分别进行排序,然后将排序好的两部分合并。
  • 堆排序:利用堆这种数据结构所设计的一种排序算法。

7.2 外部排序

外部排序算法是指在排序过程中,数据量太大,无法全部加载到内存中,需要使用外部存储(如硬盘)的排序方法。这类算法通常涉及到将数据分块逐步加载到内存中进行排序,然后再将排序好的块合并。

外部排序通常涉及以下几个步骤:

  1. 分割:将大文件分割成可以一次性装入内存的多个小文件。
  2. 排序:使用内部排序算法对每个小文件进行排序。
  3. 归并:将排序好的小文件逐步归并成最终的大文件。

外部排序算法通常需要考虑磁盘I/O操作,因此算法的设计会尽量减少I/O次数,以提高排序效率。

7.3注意事项

  • 内存限制:内部排序不需要考虑内存限制,而外部排序需要处理大量数据时的内存管理。
  • I/O效率:外部排序算法的性能很大程度上取决于磁盘I/O效率。
  • 算法选择:根据数据规模和可用内存选择最合适的排序算法。
  • 稳定性:如果需要保持相等元素的原始顺序,应选择稳定的排序算法。

具体排序方法可根据需要,检索学习。

排序算法的选择取决于多种因素,包括数据规模、内存可用性、是否需要稳定性以及对性能的要求。理解内部排序和外部排序的区别有助于在不同场景下选择合适的排序策略。

8 查找

查找操作是计算机科学中的一个基础概念,它涉及在数据结构中搜索特定的元素或数据。在数组中进行查找时,通常会用到以下几种查找方法:

8.1 线性查找(顺序查找)

线性查找是通过遍历数组中的每个元素来查找特定值的方法。这是最简单但效率较低的查找方法,特别是在大型数组中。

实现示例

public static int linearSearch(int[] array, int target) {
    for (int i = 0; i < array.length; i++) {
        if (array[i] == target) {
            return i; // 返回目标元素的索引
        }
    }
    return -1; // 如果没有找到,返回-1
}

8.2 二分查找

二分查找是在有序数组中查找特定元素的一种非常高效的算法。通过比较数组中间的元素与目标值,可以决定是继续在左半部分还是右半部分进行查找,然后重复这个过程,直到找到目标值或查找范围为空。

实现示例

public static int binarySearch(int[] array, int target) {
    int left = 0;
    int right = array.length - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (array[mid] == target) {
            return mid; // 找到目标元素,返回索引
        } else if (array[mid] < target) {
            left = mid + 1; // 在右半部分继续查找
        } else {
            right = mid - 1; // 在左半部分继续查找
        }
    }
    
    return -1; // 如果没有找到,返回-1
}

8.3 插值查找

插值查找是二分查找的一种改进,它考虑了数组元素的分布情况,通过目标值在数组中的可能位置来进行查找,适用于元素分布均匀的有序数组。

实现示例

public static int interpolationSearch(int[] array, int target) {
    int low = 0;
    int high = array.length - 1;
    
    while (low <= high && target >= array[low] && target <= array[high]) {
        int pos = low + ((target - array[low]) * (high - low)) / (array[high] - array[low]);
        
        if (array[pos] == target) {
            return pos;
        }
        
        if (array[pos] < target) {
            low = pos + 1;
        } else {
            high = pos - 1;
        }
    }
    
    return -1;
}

8.4注意事项

  • 数组顺序:线性查找不需要数组有序,而二分查找和插值查找需要。
  • 性能:线性查找的时间复杂度为O(n),二分查找为O(log n),插值查找在最坏情况下可能退化为O(n),但在均匀分布的数据中通常接近O(log log n)。
  • 实现复杂度:线性查找最容易实现,而二分查找和插值查找需要更多的逻辑。
  • 适用场景:对于小型或无序数组,线性查找可能是一个简单有效的选择;对于大型且有序的数组,二分查找和插值查找更合适。

查找算法是数据处理中不可或缺的一部分,选择合适的查找算法可以显著提高程序的性能。

9 多维数组 - 二维数组

二维数组可以被看作是一个表格,其中包含了行和列,它在编程中常用于表示矩阵或进行复杂的数据处理。Java中的二维数组实际上是一维数组的"数组",即数组的每个元素本身也是一个数组。

9.1 二维数组的声明和初始化

声明二维数组

int[][] twoDimArray;

静态初始化

int[][] twoDimArray = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

动态初始化

twoDimArray = new int[3][3]; // 创建一个3行3列的二维数组

9.2 二维数组的使用

访问二维数组的元素

int element = twoDimArray[0][1]; // 访问第一行第二列的元素,即2

遍历二维数组

for (int i = 0; i < twoDimArray.length; i++) { // 遍历行
    for (int j = 0; j < twoDimArray[i].length; j++) { // 遍历列
        System.out.print(twoDimArray[i][j] + " ");
    }
    System.out.println();
}

9.3 二维数组的应用案例

矩阵乘法

矩阵乘法是二维数组的一个典型应用,下面是一个简单的矩阵乘法实现:

public class MatrixMultiplication {
    public static void main(String[] args) {
        int[][] matrixA = {
            {1, 2},
            {3, 4}
        };
        
        int[][] matrixB = {
            {5, 6},
            {7, 8}
        };
        
        int[][] result = multiplyMatrices(matrixA, matrixB);
        
        for (int i = 0; i < result.length; i++) {
            for (int j = 0; j < result[i].length; j++) {
                System.out.print(result[i][j] + " ");
            }
            System.out.println();
        }
    }
    
    public static int[][] multiplyMatrices(int[][] a, int[][] b) {
        int rowsA = a.length;
        int colsA = a[0].length;
        int rowsB = b.length;
        int colsB = b[0].length;
        
        // 确保矩阵乘法是可行的
        if (colsA != rowsB) {
            throw new IllegalArgumentException("矩阵A的列数必须与矩阵B的行数相等。");
        }
        
        int[][] product = new int[rowsA][colsB];
        for (int i = 0; i < rowsA; i++) {
            for (int j = 0; j < colsB; j++) {
                for (int k = 0; k < colsA; k++) {
                    product[i][j] += a[i][k] * b[k][j];
                }
            }
        }
        return product;
    }
}

9.4注意事项

  • 初始化一致性:在静态初始化时,所有子数组必须具有相同的长度。
  • 内存布局:二维数组在内存中并不是一个连续的块,而是由多个数组组成的。
  • 性能:对于大型矩阵操作,要考虑性能和内存使用。
  • 错误处理:在执行如矩阵乘法等操作时,要检查操作的可行性(例如,矩阵的维度是否匹配)。

二维数组是处理表格数据的强大工具,理解其声明、初始化和操作对于编程中解决实际问题非常重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值