Skip to content

数据结构和算法之旅(一)- 数组

约 1729 字大约 6 分钟

数据结构数组

2016-12-19

没有人可以避免使用数组。

在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引来标识;因为数组内元素是连续存储的,所以数组中的元素的地址,可以通过其索引计算出来,例如:

数组的特点:随机访问。即根据索引查找元素,时间复杂度是O(1)。

自定义动态数组

数组是静态数组,不能够动态调整大小。Java是有提供好的动态数组,其实就是ArrayList,但目前是为了学习数据结构,下面自己来实现一个动态数组。

package org.example.datastructure;

import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.stream.IntStream;

/**
 * 动态数组
 */
public class DynamicArray implements Iterable<Integer> {

    private int size = 0;//逻辑大小
    private int capacity = 8;//容量 ,java中arraylist默认是10

    //    private int[] array = new int[capacity];//延迟加载
    private int[] array = {};

    /**
     * 添加元素到数组末尾
     *
     * @param element 待添加的元素
     */
    public void addLast(int element) {
        //array[size] = element;
        //size++;
        add(size, element);
    }

    /**
     * 添加元素到指定位置
     *
     * @param index   索引位置
     * @param element 待添加的元素
     * 时间复杂度
     *    头部插入:O(n)
     *    中间插入:O(n)
     *    尾部插入:O(1)
     */
    public void add(int index, int element) {
        //扩容检查
        checkAndGrow();
        //检查index是否合法
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("index:" + index + ",size:" + size);
        }
        //拷贝,把目标index后的元素都往后移动一位
        System.arraycopy(array, index, array, index + 1, size - index);
        array[index] = element;
        size++;
    }

    /**
     * 扩容检查
     */
    private void checkAndGrow() {
        if (size == 0) {
            array = new int[capacity];
        } else if (size == capacity) {
            //扩容,1.5倍、1.618倍、2倍:建议这些
            capacity += capacity >> 1;
            int[] newArray = new int[capacity];
            //拷贝
            System.arraycopy(array, 0, newArray, 0, size);
            //替换
            array = newArray;
        }
    }

    /**
     * 删除指定位置的元素
     *
     * @param index
     * @return
     */
    public int remove(int index) {//[0..size)
        int removed = array[index];
        if (index < size - 1) {
            //拷贝,把目标index后的元素都往前移动一位
            System.arraycopy(array, index + 1, array, index, size - index - 1);
            size--;
        }
        return removed;
    }

    /**
     * 查询指定位置的元素
     *
     * @param index 索引
     *
     * 时间复杂度:O(1)
     */
    public int get(int index) {// [0..size)
        return array[index];
    }

    /**
     * 使用函数式接口Consumer,遍历数组,
     * 对每个元素执行consumer.accept(array[i]),令调用方执行自定义的操作
     *
     * @param consumer
     */
    public void foreach(Consumer<Integer> consumer) {
        for (int i = 0; i < size; i++) {
            // 提供array[i]
            // 返回void
            consumer.accept(array[i]);
        }
    }

    /**
     * 迭代器遍历
     * 实现Iterable接口,使得DynamicArray可以使用foreach语法
     */
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            private int index = 0;

            @Override
            public boolean hasNext() {
                return index < size;//index < size表示还有元素
            }

            @Override
            public Integer next() {
                return array[index++];
            }
        };
    }

    /**
     * 获取流对象
     */
    public IntStream stream() {
        return IntStream.of(Arrays.copyOfRange(array, 0, size));
    }
}

System.arraycopy

注: System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length); - src:源数组,即要复制的数组。 - srcPos:源数组的起始位置,从源数组的哪个索引开始复制。 - dest:目标数组,即复制到的数组。 - destPos:目标数组的起始位置,复制到目标数组的哪个索引位置。 - length:要复制的元素数量。 System.arrayCopy 的主要特点和注意事项: 1. **低级别操作:** 这是一种低级别的数组操作,直接在内存中复制数组内容,效率较高。 2. **复制范围控制:** 你可以控制复制的范围,包括源数组的起始位置、目标数组的起始位置和要复制的元素数量。 3. **原地复制:** 它允许在不创建新数组的情况下,将源数组的一部分复制到目标数组中,适用于需要在原地进行数据移动的情况。 4. **数据类型检查:** `System.arrayCopy` 不会进行数据类型检查,因此要确保源和目标数组的数据类型一致,否则可能会导致运行时异常。 5. **不会自动扩展:** 它不会自动扩展目标数组,如果目标数组长度不足以容纳复制的数据,会导致数组越界异常。 6. **效率高:** 由于是底层操作,因此通常情况下比使用迭代或循环来复制数组要更高效

函数式接口

这个很有趣,推荐平常多使用函数式接口来优化代码,这里总结汇总一下吧。

Runnable 和 Callable

  • Runnable 通常用于多线程编程,用于定义线程要执行的任务。它没有输入参数,也没有返回值。
  • Callable 类似于 Runnable,但允许任务返回结果。通常与线程池一起使用,可以获得任务执行的结果。
// 使用Runnable创建线程任务
Runnable runnable = () -> {
    System.out.println("Hello from Runnable");
};
Thread thread = new Thread(runnable);
thread.start();

// 使用Callable和ExecutorService获取线程任务的返回值
Callable<Integer> callable = () -> {
    return 42;
};
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> result = executorService.submit(callable);
System.out.println("Result from Callable: " + result.get());
executorService.shutdown();

Comparator

  • Comparator 用于比较两个对象的顺序。它通常用于集合的排序操作。
  • 可以使用 Comparatorcomparing 方法创建比较器,也可以使用 reversed 方法反转比较器的顺序。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.sort((s1, s2) -> s1.compareTo(s2));
// 或者使用Comparator.comparing方法
names.sort(Comparator.comparing(String::length));

Consumer

  • 直译有"消费者"的含义。
  • Consumer 用于接受一个参数并执行操作,通常没有返回值。它常用于集合的遍历和元素处理。
  • 可以使用 andThen 方法组合多个 Consumer,形成一个连续的操作链。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Consumer<Integer> printSquare = (num) -> System.out.println(num * num);
numbers.forEach(printSquare);

Supplier

  • 直译有“供应者”的含义。
  • Supplier 通常不接受参数,用于生成一个值。它可以用于延迟加载或惰性计算。
  • 可以在需要值的地方调用 get 方法来获取值
Supplier<Double> randomNumberSupplier = () -> Math.random();
double randomValue = randomNumberSupplier.get();
System.out.println("Random Value: " + randomValue);

Predicate

  • 直译有"谓语"的含义。
  • Predicate 接受一个参数并返回布尔值,用于进行条件判断。它通常用于过滤和筛选数据。
  • 可以使用 andornegate 方法来组合多个谓词。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Predicate<Integer> isEven = (num) -> num % 2 == 0;
List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());

Function

  • Function 接受一个参数并返回一个值,用于将输入映射到输出。它通常用于数据转换和处理。
  • 可以使用 andThencompose 方法来组合多个函数,形成一个函数链。
Function<Integer, String> intToString = (i) -> String.valueOf(i);
String stringValue = intToString.apply(42);
System.out.println("String Value: " + stringValue);

BiFunction

  • BiFunction 接受两个参数并返回一个值,用于处理两个输入并产生一个输出。它通常用于需要两个输入的操作。
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(3, 5);
System.out.println("Sum: " + sum);