【数据结构】-4-顺序表(上)

1、顺序表的核心结构定义

顺序表本质是基于数组实现的线性表,核心需要三个要素:存储数据的容器、记录元素个数的变量和默认容量。

java 复制代码
public class MyArrayList {
    // 存储数据的数组,是顺序表的底层容器
    private int[] elem;
    // 记录有效元素的个数,也是下一个可插入位置的索引
    private int usedSize;
    // 默认容量,当不指定容量时使用
    private static final int DEFAULT_SIZE = 10;
    
    // ... 后续方法实现
}

这部分代码定义了顺序表的基本结构:

  • elem 数组作为底层存储容器,利用其连续内存的特性
  • usedSize 既表示元素个数,也作为下一个元素的插入位置
  • DEFAULT_SIZE 提供默认初始容量

2、构造方法:初始化顺序表

顺序表需要初始化才能使用,我们提供两种初始化方式:

java 复制代码
// 无参构造方法,使用默认容量初始化
public MyArrayList() {
    this.elem = new int[DEFAULT_SIZE];
    this.usedSize = 0;
}

// 带参构造方法,指定容量初始化
public MyArrayList(int capacity) {
    if (capacity <= 0) {
        throw new IllegalArgumentException("容量不能为负数或零: " + capacity);
    }
    this.elem = new int[capacity];
    this.usedSize = 0;
}

构造方法的作用是初始化数组并设置初始元素个数为 0:

  • 无参构造使用默认容量 10
  • 带参构造允许自定义容量,但会检查容量合法性
  • 初始时 usedSize 都为 0,表示还没有元素

3、扩容机制:动态调整容量

数组容量固定,当元素存满时需要扩容:

java 复制代码
// 检查是否需要扩容
private void checkCapacity() {
    if (usedSize == elem.length) {
        // 扩容为原来的1.5倍
        int newCapacity = elem.length * 3 / 2;
        elem = Arrays.copyOf(elem, newCapacity);
    }
}

扩容逻辑说明:

  • usedSize 等于数组长度时,表示数组已满
  • 新容量为原容量的 1.5 倍(length * 3 / 2
  • 使用 Arrays.copyOf() 复制原数组内容到新数组
  • 扩容操作的时间复杂度是 O (n),因为需要复制所有元素

4、插入操作:在指定位置添加元素

插入是顺序表的核心操作之一,分为指定位置插入和末尾插入:

java 复制代码
// 在指定位置插入元素
public void add(int index, int data) {
    // 检查索引是否合法(0 ≤ index ≤ usedSize)
    if (index < 0 || index > usedSize) {
        throw new IndexOutOfBoundsException("索引越界: " + index);
    }
    
    // 检查是否需要扩容
    checkCapacity();
    
    // 将index位置及之后的元素后移,为新元素腾出位置
    for (int i = usedSize - 1; i >= index; i--) {
        elem[i + 1] = elem[i];
    }
    
    // 插入新元素
    elem[index] = data;
    usedSize++;
}

// 在末尾添加元素(简化版)
public void add(int data) {
    add(usedSize, data); // 直接调用指定位置插入,位置为usedSize
}

顺序表中,每个元素(除第一个外)都必须有直接前一个元素(前驱)。如果在索引 5 插入元素,而索引 3、4 为空,那么这个新元素就没有合法的前驱元素(索引 4 的位置为空,不能作为前驱),这违反了顺序表的逻辑结构。

插入操作的关键步骤:

  1. 检查索引合法性,确保插入位置有效
  2. 检查容量,不足则扩容
  3. 从最后一个元素开始,将 index 及之后的元素依次后移一位
  4. index 位置插入新元素,更新 usedSize
  5. 时间复杂度为 O (n),因为最坏情况下需要移动所有元素

5、删除操作:移除指定位置的元素

删除操作与插入类似,也需要移动元素:

java 复制代码
// 删除指定位置的元素
public int remove(int index) {
    // 检查索引是否合法(0 ≤ index < usedSize)
    if (index < 0 || index >= usedSize) {
        throw new IndexOutOfBoundsException("索引越界: " + index);
    }
    
    // 保存要删除的元素,用于返回
    int oldData = elem[index];
    
    // 将index之后的元素前移,覆盖被删除的元素
    for (int i = index; i < usedSize - 1; i++) {
        elem[i] = elem[i + 1];
    }
    
    usedSize--; // 元素个数减1
    return oldData; // 返回被删除的元素
}

删除操作的特点:

  • 索引合法性检查与插入不同,index 不能等于 usedSize
  • 需要保存被删除的元素以便返回
  • 从删除位置开始,将后面的元素依次前移一位
  • 时间复杂度为 O (n),同样可能需要移动多个元素

6、查找与访问操作

顺序表支持按索引访问和按值查找:

java 复制代码
// 查找元素的索引
public int indexOf(int data) {
    for (int i = 0; i < usedSize; i++) {
        if (elem[i] == data) {
            return i;
        }
    }
    return -1; // 找不到返回-1
}

// 获取指定位置的元素
public int get(int index) {
    if (index < 0 || index >= usedSize) {
        throw new IndexOutOfBoundsException("索引越界: " + index);
    }
    return elem[index];
}

// 修改指定位置的元素
public void set(int index, int data) {
    if (index < 0 || index >= usedSize) {
        throw new IndexOutOfBoundsException("索引越界: " + index);
    }
    elem[index] = data;
}

这些操作的特点:

  • get()set() 按索引操作,时间复杂度为 O (1)
  • indexOf() 按值查找,需要遍历元素,时间复杂度为 O (n)
  • 都需要先检查索引合法性

7、辅助方法

一些常用的工具方法:

java 复制代码
// 判断顺序表是否为空
public boolean isEmpty() {
    return usedSize == 0;
}

// 获取顺序表的长度
public int size() {
    return usedSize;
}

// 清空顺序表(仅重置元素个数,不释放内存)
public void clear() {
    usedSize = 0;
}

// 打印顺序表
public void display() {
    for (int i = 0; i < usedSize; i++) {
        System.out.print(elem[i] + " ");
    }
    System.out.println();
}

这些方法提供了顺序表的基本管理功能:

  • isEmpty()size() 用于判断状态和获取长度
  • clear() 清空操作非常高效,只需重置 usedSize
  • display() 用于查看顺序表内容

8、使用示例

java 复制代码
public static void main(String[] args) {
    MyArrayList list = new MyArrayList();
    
    // 添加元素
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(1, 4); // 在索引1的位置插入4
    
    System.out.print("顺序表元素: ");
    list.display(); // 输出: 1 4 2 3 
    
    // 获取元素
    System.out.println("索引2的元素: " + list.get(2)); // 输出: 2
    
    // 修改元素
    list.set(2, 5);
    System.out.print("修改后元素: ");
    list.display(); // 输出: 1 4 5 3 
    
    // 查找元素
    System.out.println("元素5的索引: " + list.indexOf(5)); // 输出: 2
    
    // 删除元素
    list.remove(1);
    System.out.print("删除后元素: ");
    list.display(); // 输出: 1 5 3 
    
    // 顺序表长度
    System.out.println("顺序表长度: " + list.size()); // 输出: 3
}

通过这个完整实现,我们可以清晰地看到顺序表的工作原理:基于数组存储,通过索引快速访问,插入删除需要移动元素,支持动态扩容。这种结构适合频繁访问但不频繁插入删除的场景。

二、异常处理(自定义异常 PosIllegality)

代码里自定义了 PosIllegality 异常类,用于处理顺序表插入时的下标非法问题,核心逻辑拆解:

1. 自定义异常类 PosIllegality
java 复制代码
public class PosIllegality extends RuntimeException{
    public PosIllegality(String msg) {
        super(msg); 
    }
}
  • 继承 RuntimeException,属于运行时异常 (无需强制 try-catch,但建议主动处理)。
  • 构造方法接收错误信息 msg,调用父类构造传递信息,方便抛出时提示 "哪里错了"。
2. 异常触发:checkPosOnAdd 方法
python 复制代码
private void checkPosOnAdd(int pos) throws PosIllegality{
    if(pos < 0 || pos > usedSize) { 
        System.out.println("不符合法!");
        throw new PosIllegality("插入元素下标异常:"+pos); 
    }
}
  • 作用:检查插入位置 pos 是否合法(范围 [0, usedSize],因为顺序表允许 "尾部插入",pos == usedSize 是末尾新增)。
  • 触发条件:pos 小于 0,或大于当前有效元素个数 usedSize(比如顺序表已有 3 个元素,usedSize=3pos=4 合法,是末尾插入;但 pos=5 就非法)。
  • 执行逻辑:一旦非法,先打印提示,再抛出 PosIllegality 异常,中断正常流程,告诉调用者 "这里出错了"。
3. 异常捕获与处理
java 复制代码
@Override
public void add(int pos, int data) {
    try {
        checkPosOnAdd(pos); // 尝试执行,可能抛异常
    }catch (PosIllegality e) { // 捕获自定义异常
        e.printStackTrace(); // 打印异常堆栈(方便调试,看哪里错)
        return; // 直接返回,不执行后续插入逻辑
    }
    checkCapacity(); 
    // ... 省略未写完的插入逻辑
}
  • try-catch 作用:拦截 checkPosOnAdd 抛出的 PosIllegality 异常,避免程序崩溃。
  • 处理方式:
    • e.printStackTrace():打印异常信息到控制台(包含异常类型、错误信息、调用栈),方便开发者定位问题。
    • return:捕获异常后直接返回,跳过后续插入逻辑(因为下标非法,插入无法正确执行)。

异常处理流程总结

  • 1.执行 checkPosOnAdd,检查下标合法性
  • → 2. 非法则抛出 PosIllegality
  • → 3. 被 add 方法的 try-catch 捕获
  • → 4. 打印异常并终止插入流程。
  • 作用是提前拦截非法操作,用自定义异常清晰提示 "下标有问题",让程序更健壮、报错更直观。

三、contains 方法与 equals 重写

图三代码是判断顺序表是否包含某个元素:

java 复制代码
public boolean contains(int toFind) {
    if(isEmpty()) {
        return false;
    }
    for (int i = 0; i < usedSize; i++) {
        // 如果是查找引用数据类型 一定记住 重写方法
        if(elem[i] == toFind) { 
            return true;
        }
    }
    return false;
}

当前用 == 比较,只适合 基本数据类型(如 intchar 等) 。如果存储引用数据类型(如 String、自定义类 Person 等) ,必须重写 equals,否则会出错!

为什么引用类型要重写 equals
  • 基本类型(int):== 比较是否相等。
  • 引用类型(如 String、自定义类):== 比较地址(是否是同一个对象),而非内容。

举个栗子(假设存储 String):

java 复制代码
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // false!因为是两个不同对象,地址不同

此时用 == 判断 "包含",即使内容相同,也会返回 false,逻辑错误。

如何重写 equals(以自定义类 Person 为例)

假设顺序表存 Person 对象,需要判断是否包含 "相同内容" 的 Person

自定义类 Person

java 复制代码
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 必须重写equals!
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; // 地址相同,直接返回true
        if (obj == null || getClass() != obj.getClass()) return false; // 类型不同,返回false
        Person person = (Person) obj; // 强转成Person
        // 比较内容(name和age是否相等)
        return age == person.age && Objects.equals(name, person.name); 
    }
}

修改 contains 方法(适配引用类型)

如果顺序表存 Personcontains 要改成:

java 复制代码
public boolean contains(Person toFind) {
    if(isEmpty()) {
        return false;
    }
    for (int i = 0; i < usedSize; i++) {
        // 用equals比较内容,而非==
        if(elem[i].equals(toFind)) { 
            return true;
        }
    }
    return false;
}

四、clear()方法

基本数据类型数组可以直接usedSize置零,12 23 这些数字虽然还在里面,但是后续添加数组就会把他们挤掉。就像领导想裁员,也没必要直接叫牛马走,招来新人了直接就给换了,换不换人不在乎人是否还在工位上,而是领导想换人的那个心思(usedSize就是领导的心思)

但要是引用数据类型的数组,每个元素的下标会指向一个对象,对象的空间是存堆上的,给usedSize置零的意图就是不用这些引用了,但是他们的对象又有引用指着,不会被回收,占用内存。

1、核心场景:顺序表存引用类型(Person[] elem

顺序表底层用数组 elemPerson 对象,但数组里存的是 对象的引用(地址),而非对象本身。

  • 比如图中 elem[0]0x88elem[1]0x67,这些是堆中 Person 对象的地址。
  • 堆里的 Person 对象(标 0x880x67 的椭圆),真正存着 nameage 等数据。

2、"清空顺序表" 的内存问题:为何要手动置 null

如果直接把 usedSize 置 0(只改有效元素个数),数组里的引用还在elem[0] 仍指向 0x88elem[1] 仍指向 0x67)。

  • 这些 Person 对象因被数组引用,无法被 JVM 垃圾回收(GC 只回收 "无任何引用指向" 的对象)。
  • 长期这样会导致 内存泄漏(无用对象占着内存不释放,消耗资源)。

3、正确的 "清空" 逻辑(让 GC 能回收对象)

要让 GC 回收这些 Person 对象,需切断数组对它们的引用,步骤:

  1. 遍历数组,手动置 null

    java 复制代码
    for (int i = 0; i < usedSize; i++) {
        elem[i] = null; // 切断引用,让Person对象失去"指向"
    }

    这样,堆中的 Person 对象(如 0x880x67)就没有引用指向它们了。

  2. 重置 usedSize

    java 复制代码
    usedSize = 0; // 标记顺序表"无有效元素"
  3. (可选)置空数组引用

    java 复制代码
    elem = null; // 连数组本身的引用也切断(极端情况用,一般步骤1+2足够)

五、ArrayList 无参构造方法

1、无参构造初始化:空数组占位,延迟分配内存

java 复制代码
// 无参构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 静态空数组,作为无参构造的初始值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  • 初始化行为 :调用 new ArrayList<>() 时,并不会直接分配默认容量(10)的数组,而是让 elementData 指向空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 设计意图 :这是一种延迟初始化 策略 ------ 先占个 "空数组" 的位置,等真正需要存数据(调用 add)时,再分配内存,避免创建空列表时浪费空间。

2、第一次 add 元素:触发内存分配(默认容量 10)

当执行 list.add(1) 这类添加操作时,会进入 add 方法核心逻辑:

java 复制代码
public boolean add(E e) {
    // 计算最小需要的容量,第一次 add 时 size=0,minCapacity=1
    ensureCapacityInternal(size + 1); 
    // 赋值并递增 size
    elementData[size++] = e; 
    return true;
}

关键方法 ensureCapacityInternal

java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    // 计算真正需要的容量(第一次 add 时触发特殊逻辑)
    minCapacity = calculateCapacity(elementData, minCapacity); 
    ensureExplicitCapacity(minCapacity);
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是无参构造的空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
        // 返回默认容量(10)和 minCapacity 的较大值
        return Math.max(DEFAULT_CAPACITY, minCapacity); 
    }
    return minCapacity;
}
  • 第一次 addelementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组),所以 calculateCapacity 会返回 Math.max(10, 1)10DEFAULT_CAPACITY 是 10)。
  • 效果 :第一次添加元素时,才真正分配容量为 10 的数组,体现 "延迟分配" 的设计,避免空列表占内存。

3、扩容逻辑:1.5 倍扩容,应对容量不足

当添加元素导致 elementData 装满时(如第 11 次 add),会触发扩容:

java 复制代码
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果需要的容量 > 数组当前长度,触发扩容
    if (minCapacity - elementData.length > 0) { 
        grow(minCapacity);
    }
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length; 
    // 新容量 = 旧容量 + 旧容量的 1/2 → 1.5 倍扩容
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    // 如果新容量不够(比如旧容量是 0 时),直接用 minCapacity
    if (newCapacity - minCapacity < 0) { 
        newCapacity = minCapacity;
    }
    // 超过最大限制(Integer.MAX_VALUE - 8)时,特殊处理
    if (newCapacity - MAX_ARRAY_SIZE > 0) { 
        newCapacity = hugeCapacity(minCapacity);
    }
    // 复制数组到新容量
    elementData = Arrays.copyOf(elementData, newCapacity); 
}
  • 核心规则 :扩容时新容量是旧容量的 1.5 倍 (通过位运算 oldCapacity >> 1 实现,等价于除以 2 取整)。

  • 边界处理

    • 若 1.5 倍扩容后仍不够,直接用 minCapacity(比如初始空数组第一次扩容)。
    • 若超过 MAX_ARRAY_SIZEInteger.MAX_VALUE - 8,避免内存溢出),则用 Integer.MAX_VALUE 或抛出异常。

4、完整流程总结(无参构造 + 添加元素)

  • 初始化new ArrayList<>()elementData 指向空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,不分配实际内存。
  • 第一次 add
    • 调用 ensureCapacityInternal → 触发 calculateCapacity,因是空数组,返回默认容量 10。
    • 分配容量为 10 的数组,存入元素,size 变为 1。
  • 后续 add
    • 若数组未满(size < elementData.length),直接存数据。
    • 若数组已满(size == elementData.length),触发 grow 方法,1.5 倍扩容后复制数组,再存数据。

核心设计亮点

  • 延迟初始化 :无参构造不占内存,第一次 add 才分配默认容量,节省空间。
  • 动态扩容:1.5 倍扩容平衡了性能(减少扩容次数)和内存消耗(避免过度分配)。
  • 边界处理:针对极端情况(容量过小、过大)做了防护,保证稳定性。

六、ArrayList 接收 Collection 参数的构造方法

用于从其他集合快速初始化 ArrayList

1、构造方法定义与泛型通配符

java 复制代码
public ArrayList(Collection<? extends E> c) {
    // ... 构造逻辑
}
  • 泛型通配符 <? extends E>

    表示传入的 Collection 集合,其元素类型必须是 E 的子类(或 E 本身)。这是上界通配符 ,保证类型兼容,让 ArrayList 能接收 "元素类型是 E 及其子类" 的集合。

    比如示例中:

    • listArrayList<Integer>EInteger)。
    • list12ArrayList<Number>ENumber),而 IntegerNumber 的子类,所以 list 能传给 list12 的构造方法(满足 ? extends Number)。

2、构造方法核心逻辑

构造方法内部做了这些事,把传入的 Collection 转成 ArrayList 内部数组:

java 复制代码
elementData = c.toArray(); // 1. 转成数组
if ((size = elementData.length) != 0) { // 2. 检查数组长度
    // 修复 toArray 可能返回非 Object[] 的问题(比如某些集合实现的特殊情况)
    if (elementData.getClass() != Object[].class) 
        elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
    // 空集合,直接指向空数组
    this.elementData = EMPTY_ELEMENTDATA;
}
  • 转成数组c.toArray() 把传入的集合 c 转成数组,存入 elementDataArrayList 底层存储数组)。

  • 处理长度

    • 若数组长度 size != 0
      • 额外检查 elementData 的类型是否是 Object[](防止某些集合的 toArray 返回特殊类型数组,比如 String[] 转成 ArrayList<Object> 时的兼容问题)。
      • 不匹配则用 Arrays.copyOf 转成标准 Object[],保证后续操作安全。
    • 若数组长度 size == 0:直接让 elementData 指向静态空数组 EMPTY_ELEMENTDATA,节省内存。

3、示例代码关联

java 复制代码
// list 是 ArrayList<Integer>,元素类型 Integer(E 为 Integer)
ArrayList<Integer> list = new ArrayList<>(); 
list.add(1); list.add(2); list.add(3);

// list12 是 ArrayList<Number>,元素类型 Number(E 为 Number)
// 因 Integer 是 Number 的子类,满足 <? extends Number>,所以 list 能传入构造方法
ArrayList<Number> list12 = new ArrayList<>(list); 
  • 类型兼容list 的元素是 IntegerNumber 的子类),符合 ArrayList<Number> 构造方法中 Collection<? extends Number> 的要求。
  • 效果list12 会把 list 的元素(1、2、3)复制过来,底层通过 c.toArray() 转成数组,再初始化 elementData,实现从其他集合快速构建 ArrayList

4、设计意图与作用

这个构造方法主要解决 "从已有集合快速初始化 ArrayList" 的需求:

  1. 灵活兼容 :通过 <? extends E> 通配符,允许传入 "元素是 E 子类" 的集合,拓宽使用场景(比如用 Integer 集合初始化 Number 集合的 ArrayList)。
  2. 处理边界 :修复 toArray 可能返回非 Object[] 的问题,保证 ArrayList 底层数组类型正确,避免后续操作(如添加、遍历)报错。
  3. 性能优化 :直接基于已有集合的数组转换,减少额外拷贝(除特殊情况的 Arrays.copyOf),提升初始化效率。

三、集合底层数组接收 需要类型转换原理

Java 中,Collection 接口的 toArray() 方法默认返回 Object[] 数组,但某些集合的实现类(如 ArrayList 的子类,或自定义集合)可能重写 toArray(),返回具体类型的数组 (比如 String[]Integer[])。

java 复制代码
// 创建一个存储 String 的 ArrayList
ArrayList<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");

// 调用 toArray(),返回的实际是 String[] 类型(而非 Object[])
Object[] arr = strList.toArray(); 
System.out.println(arr.getClass()); // 输出:class [Ljava.lang.String;(String数组)
  • toArray() 方法的源码逻辑是:直接返回底层数组的 "拷贝",而拷贝的数组类型会保留元素的实际类型。

简单说:
ArrayList<String> 中存的都是 String,它的 toArray() 会返回一个 "装着 String 的数组" ,这个数组在内存中就是 String[] 类型(即使编译时声明为 Object[])。

冲突场景:当用 String[] 初始化 ArrayList<Object>

假设我们用上面的 strList 去初始化一个 ArrayList<Object>

java 复制代码
// 用 String 集合初始化 Object 类型的 ArrayList
ArrayList<Object> objList = new ArrayList<>(strList);

此时,strList.toArray() 返回的是 String[] 数组,而 objList 的底层数组 elementData 理论上应该是 Object[] 类型(因为要存储任意 Object 子类对象)。

如果不做处理,objList.elementData 会直接指向 String[] 数组,后续操作会出问题:

java 复制代码
// 尝试往 objList 中添加一个 Integer 对象(合法,因为 Integer 是 Object 子类)
objList.add(123); // 编译通过,但运行时会报错!

报错原因String[] 数组只能存储 String 类型,强行存入 Integer 会触发 ArrayStoreException(数组存储异常)。

解决方案:额外检查并转换为 Object[]

正是为了避免上述问题,ArrayList 的构造方法中加入了类型检查:

java 复制代码
// 如果原数组不是 Object[] 类型,就转换为 Object[]
if (elementData.getClass() != Object[].class) 
    elementData = Arrays.copyOf(elementData, size, Object[].class);
  • toArray() 返回的是 String[] 时,elementData.getClass() != Object[].class 成立,触发转换。
  • Arrays.copyOf(..., Object[].class) 会创建一个新的 Object[] 数组,并复制原数组元素,保证 elementDataObject[] 类型。

这个细节体现了 Java 泛型和数组类型系统的严谨性,避免了因类型不匹配导致的运行时异常。

相关推荐
老赵的博客15 分钟前
QT的项目pro qmake编译
开发语言·qt
SimonKing25 分钟前
无需重启!动态修改日志级别的神技,运维开发都哭了
java·后端·程序员
杨杨杨大侠1 小时前
第3篇:配置管理的艺术 - 让框架更灵活
java·spring·log4j
野生的编程萌新1 小时前
【数据结构】从基础到实战:全面解析归并排序与计数排序
数据结构·算法·排序算法
枯萎穿心攻击1 小时前
从 Unity UGUI 到 Unreal UMG 的交互与高效实践:UI 事件、坐标系适配与性能优化
开发语言·ui·unity·性能优化·ue5·游戏引擎·虚幻引擎
WALL-EC1 小时前
Qt工具栏中图标槽函数没有响应的问题分析
开发语言·qt·osgearth
WCL-JAVA1 小时前
java生成带水印的pdf文件
java·python·pdf
pengzhuofan2 小时前
Java设计模式-模板方法模式
java·设计模式·模板方法模式
Java中文社群2 小时前
重磅!Ollama发布UI界面,告别命令窗口!
java·人工智能·后端
YuTaoShao2 小时前
【LeetCode 热题 100】152. 乘积最大子数组——(解法一)递推
java·算法·leetcode·职场和发展