news 2026/6/27 1:02:38

数据结构(五)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构(五)

一、快速排序算法:核心分治思想与高效实现

快速排序(Quick Sort)是经典的分治排序算法,凭借O(n log n)的平均时间复杂度、空间效率高、原地排序等优势,在工程实践中被广泛应用。它的核心是通过“基准数定位+双指针交换+递归分治”的逻辑,将大数组拆解为小数组,逐层完成排序。

核心分治思想与游标移动逻辑

快速排序的核心是分治策略:将待排序数组拆分为“基准数+左侧子数组+右侧子数组”,左侧子数组均小于基准数,右侧均大于基准数,再对左右子数组递归执行同样的操作,直至子数组不可再拆分(长度为1)。这一过程依赖基准数定位和双指针交换机制。

双指针交换是快速排序的核心操作,目的是让左右指针相向移动,找到“左侧大于基准数、右侧小于基准数”的元素对并交换,最终将基准数归位到正确的位置,完成一次分区。

指针移动逻辑: I指针(左指针):从左向右移动,寻找第一个大于基准数的元素,若找到则停止;若未找到(I超过J),直接进入最终交换。 J指针(右指针):从右向左移动,寻找第一个小于基准数的元素,若找到则停止;若未找到(J超过I),直接进入最终交换。

交换时机:当I和J均找到符合条件的元素后,交换二者对应的数组元素,之后继续移动指针。 最终归位:当I和J相遇时,无论相遇位置的元素是大于还是小于基准数,都将基准数与相遇位置的元素交换,此时基准数就位于了分区后的正确位置。

递归终止条件

分区完成后,数组被拆分为左侧子数组(left到pivotIndex-1)和右侧子数组(pivotIndex+1到right),此时需要对这两个子数组递归执行相同的分区、交换操作,直至子数组的长度为1(此时数组自然有序,无需排序)。

递归终止条件:当待排序区间的左边界left大于等于右边界right时,说明该区间长度小于等于1,直接终止递归。 递归核心:递归的本质是“分而治之”,通过不断拆分大区间为小区间,直到小区间无需排序,最终将所有小区间有序合并,得到完整有序的数组。

完整快速排序代码

import java.util.Arrays; public class QuickSortComplete { // 快速排序入口方法 public void quickSort(int[] array, int left, int right) { // 递归出口:区间长度<=1,直接返回 if (left >= right) { return; } // 1. 分区:获取基准数归位后的下标 int pivotIndex = partition(array, left, right); // 2. 递归排序左侧子数组 [left, pivotIndex-1] quickSort(array, left, pivotIndex - 1); // 3. 递归排序右侧子数组 [pivotIndex+1, right] quickSort(array, pivotIndex + 1, right); } // 分区方法(同上述单次分区逻辑) private int partition(int[] array, int left, int right) { int pivot = array[left]; int i = left; int j = right; while (i < j) { while (i < j && array[j] >= pivot) { j--; } while (i < j && array[i] <= pivot) { i++; } if (i < j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } } array[left] = array[i]; array[i] = pivot; return i; } public static void main(String[] args) { QuickSortComplete sorter = new QuickSortComplete(); int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}; System.out.println("排序前数组:" + Arrays.toString(arr)); sorter.quickSort(arr, 0, arr.length - 1); System.out.println("排序后数组:" + Arrays.toString(arr)); // 输出:排序后数组按升序排列 } }

内存执行流程与变量作用域

快速排序的递归过程依赖JVM的栈结构,理解栈帧变化和变量作用域,能帮助我们更清晰地掌握算法的执行逻辑,避免陷入递归的“思维迷宫”。

栈帧变化模拟

递归的本质是方法的嵌套调用,每次递归调用都会在JVM的栈空间中创建一个新的栈帧,用于存储当前方法的参数、局部变量和返回地址,递归结束后栈帧出栈,释放内存。

以数组[3,1,4,2]为例,快速排序的栈帧变化流程如下:

  1. 首次调用:调用quickSort(array, 0, 3),创建第一个栈帧,存储参数left=0right=3,执行分区后,基准数3归位到下标2,递归调用quickSort(array,0,1)quickSort(array,3,3)
  2. 左子递归入栈:调用quickSort(array,0,1),创建第二个栈帧,分区后基准数1归位到下标0,递归调用quickSort(array,0,-1)quickSort(array,1,1)
  3. 左子递归的子调用入栈:quickSort(array,0,-1)触发递归出口,直接出栈;quickSort(array,1,1)也触发出口,出栈;第二个栈帧执行完毕,出栈。
  4. 右子递归入栈:回到首次调用,执行quickSort(array,3,3),触发出口,出栈。
  5. 首次调用出栈:首次调用执行完毕,栈帧出栈,完成整个排序过程。

核心要点:栈帧遵循“先进后出”的原则,递归调用时栈帧入栈,递归出口触发时栈帧出栈,递归深度不能超过JVM栈的容量(否则会抛出栈溢出异常StackOverflowError)。

变量作用域

快速排序的方法中,变量的作用域决定了变量的生命周期,需要重点关注局部变量和数组引用的差异: 局部变量(如temp):局部变量存储在当前方法的栈帧中,当方法执行结束、栈帧出栈时,局部变量会自动销毁,内存被回收。例如分区方法中的temp,仅在单次分区过程中存在,分区结束后就被销毁,不会影响其他方法的执行。 数组引用的共享性:数组是引用类型,array这个引用变量指向堆中的数组对象。在递归过程中,虽然每个栈帧都有自己独立的array引用(但指向同一个堆中的数组),因此对数组的修改是全局生效的。例如在分区方法中修改数组元素,所有递归方法中看到的数组都是修改后的状态,这也是快速排序能实现原地排序的核心原因。

二、ArrayList底层数据结构与扩容机制

数组的核心痛点是长度固定,无法动态扩容,当数据量超过数组容量时,需要手动创建新数组并复制数据,开发效率低且易出错。ArrayList作为Java中最常用的可变数组,通过封装动态扩容的数组,解决了这一痛点,同时提供了高效的增删改查操作,是日常开发中的“高频工具类”。

底层封装与扩容策略

ArrayList的核心是封装一个动态扩容的数组,通过合理的扩容策略平衡内存占用和性能开销,其底层实现围绕初始容量、扩容触发条件和扩容规则展开。

默认容量与扩容触发条件

ArrayList采用“懒加载”策略,初始时底层数组为空,当首次添加元素时才会初始化底层数组。 默认初始容量:若创建ArrayList时未指定初始容量,底层数组的默认初始容量为10(JDK 1.8+)。 扩容触发条件:当ArrayList中元素的个数(size属性,记录有效数据的数量)等于底层数组的容量时,再次添加元素会触发扩容操作。例如底层数组容量为10,当size为10时,添加第11个元素就会触发扩容。

扩容倍数规则

扩容的核心是创建新数组并复制数据,ArrayList的扩容规则兼顾了性能和内存利用率,具体规则为: 新容量约为原容量的1.5倍。 计算公式:新容量 = 原容量 + 原容量右移1位(等价于原容量1.5)。例如原容量为10,右移1位是5,新容量为10+5=15;原容量为15,右移1位是7,新容量为15+7=22。

扩容流程:

  1. 计算新容量;
  2. 创建新数组,容量为新容量;
  3. 将原数组的所有元素复制到新数组;
  4. 将ArrayList的底层数组引用指向新数组;
  5. 后续添加元素直接在新数组中进行。

代码(ArrayList扩容逻辑):

import java.util.Arrays; public class ArrayListSimulation { // 模拟ArrayList的核心属性:底层数组、有效元素个数 private Object[] elementData; private int size; // 构造方法:默认容量10 public ArrayListSimulation() { this.elementData = new Object[10]; this.size = 0; } // 构造方法:指定初始容量 public ArrayListSimulation(int initialCapacity) { if (initialCapacity < 0) { throw new IllegalArgumentException("初始容量不能为负数"); } this.elementData = new Object[initialCapacity]; this.size = 0; } // 添加元素:触发扩容的核心逻辑 public void add(Object e) { // 扩容检查:如果有效元素个数等于数组容量,需要扩容 if (size == elementData.length) { // 计算新容量:原容量 * 1.5(通过位运算实现,效率高) int newCapacity = elementData.length + (elementData.length >> 1); // 创建新数组 Object[] newArray = new Object[newCapacity]; // 复制原数组元素到新数组 System.arraycopy(elementData, 0, newArray, 0, size); // 指向新数组 elementData = newArray; System.out.println("触发扩容,原容量:" + (newCapacity - (newCapacity - elementData.length)) + ", 新容量:" + newCapacity); } // 在size位置添加元素,然后size++ elementData[size] = e; size++; } // 获取有效元素个数 public int size() { return size; } // 获取底层数组(用于测试) public Object[] getElementData() { return elementData; } public static void main(String[] args) { ArrayListSimulation list = new ArrayListSimulation(); // 添加11个元素,观察扩容过程 for (int i = 0; i < 11; i++) { list.add(i); } System.out.println("有效元素个数:" + list.size()); System.out.println("最终数组容量:" + list.getElementData().length); // 输出:添加第11个元素时触发扩容,原容量10,新容量15,最终数组容量15,有效元素个数11 } }

增删改查的底层逻辑

ArrayList基于动态数组实现了高效的增删改查操作,核心是通过size属性控制有效数据的范围,避免操作底层数组的无效空间,同时针对不同操作优化了实现逻辑。

插入操作(Add)

插入操作的核心是“先检查扩容,再在指定位置插入,最后更新size”,分为在末尾插入和在指定位置插入两种情况,其中在指定位置插入需要移动后续元素。

末尾插入:直接将元素放在elementData[size]位置,然后size++,时间复杂度O(1)。

指定位置插入:

  1. 检查插入位置的合法性(位置必须在0到size之间);
  2. 检查是否需要扩容;
  3. 将插入位置及之后的元素整体向后移动一位(从后往前移动,避免元素覆盖);
  4. 在插入位置放入新元素;
  5. size++,时间复杂度O(n)(移动元素开销)。

代码(指定位置插入逻辑):

public class ArrayListAddSimulation { private Object[] elementData = new Object[10]; private int size = 0; // 指定位置插入元素 public void add(int index, Object e) { // 1. 检查索引合法性:0 <= index <= size if (index < 0 || index > size) { throw new IndexOutOfBoundsException("索引越界,允许范围[0," + size + "]"); } // 2. 检查扩容 if (size == elementData.length) { int newCapacity = elementData.length + (elementData.length >> 1); Object[] newArray = new Object[newCapacity]; System.arraycopy(elementData, 0, newArray, 0, size); elementData = newArray; } // 3. 移动元素:从size位置到index位置,依次后移一位(从后往前遍历,避免覆盖) for (int i = size; i > index; i--) { elementData[i] = elementData[i - 1]; } // 4. 插入元素 elementData[index] = e; // 5. 更新size size++; } // 获取元素(辅助方法) public Object get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("索引越界"); } return elementData[index]; } // 转换为字符串(仅打印有效元素) @Override public String toString() { if (size == 0) return "[]"; StringBuilder sb = new StringBuilder("["); for (int i = 0; i < size; i++) { sb.append(elementData[i]); if (i != size - 1) sb.append(","); } sb.append("]"); return sb.toString(); } public static void main(String[] args) { ArrayListAddSimulation list = new ArrayListAddSimulation(); list.add(0, "A"); list.add(1, "B"); list.add(1, "C"); // 在索引1插入C,原B后移 System.out.println(list); // 输出:[A,C,B],说明插入后元素正确后移 } }
删除操作(Remove)

删除操作分为删除指定位置元素和删除指定元素,核心是“移动后续元素覆盖待删除元素,再更新size”,同时需要注意遍历方向对删除结果的影响。

删除指定位置元素:

  1. 检查索引合法性;
  2. 将待删除位置之后的所有元素整体向前移动一位(从前往后移动);
  3. 将最后一个元素置为null(帮助JVM回收,避免内存泄漏);
  4. size--,时间复杂度O(n)(移动元素开销)。

删除指定元素: 问题:若从前往后遍历,删除元素后索引会发生变化,可能导致漏删。例如数组[1,2,2,3],从前往后删除2,第一次删除索引1的2后,后续元素前移,原索引2的2变为索引1,下一次遍历到索引2,会跳过这个2,导致漏删。

解决方案:从后往前遍历,删除元素后,后续元素的位置不影响前面的遍历,可避免漏删。因为从后往前删除时,删除元素的位置之前的元素索引不会发生变化,能保证所有符合条件的元素都被删除。

代码示例

public class ArrayListRemoveSimulation { private Object[] elementData = new Object[10]; private int size = 0; // 添加元素(辅助方法) public void add(Object e) { if (size == elementData.length) { int newCapacity = elementData.length + (elementData.length >> 1); elementData = new Object[newCapacity]; } elementData[size++] = e; } // 删除所有指定元素:从后往前遍历,避免漏删 public void remove(Object e) { // 从后往前遍历,找到等于e的元素并删除 for (int i = size - 1; i >= 0; i--) { if (elementData[i] == e || (elementData[i] != null && elementData[i].equals(e))) { // 移动元素:将i之后的元素向前移动一位 for (int j = i; j < size - 1; j++) { elementData[j] = elementData[j + 1]; } // 最后一个元素置null,避免内存泄漏 elementData[size - 1] = null; size--; } } } @Override public String toString() { if (size == 0) return "[]"; StringBuilder sb = new StringBuilder("["); for (int i = 0; i < size; i++) { sb.append(elementData[i]); if (i != size - 1) sb.append(","); } sb.append("]"); return sb.toString(); } public static void main(String[] args) { ArrayListRemoveSimulation list = new ArrayListRemoveSimulation(); list.add(1); list.add(2); list.add(2); list.add(3); System.out.println("删除前:" + list); // 输出:[1,2,2,3] list.remove(2); System.out.println("删除后:" + list); // 输出:[1,3],所有2都被删除,无漏删 } }

打印重写(ToString)

ArrayList重写了toString()方法,核心是仅遍历0到size-1的有效元素,而不是遍历整个底层数组,避免打印底层数组中预留的默认值(如null或0),确保输出结果只包含用户添加的有效元素。

原因:底层数组的容量可能大于size(例如扩容后容量为15,但size为11),剩余位置是未使用的,默认值为null(对象数组),如果遍历整个数组,会输出大量无意义的null,影响可读性。

实现逻辑:重写toString()时,通过循环遍历索引0到size-1的元素,拼接字符串,仅展示有效数据。

示例

import java.util.ArrayList; public class ArrayListToStringDemo { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); // ArrayList重写toString,仅打印有效元素 System.out.println(list); // 输出:[1, 2, 3] // 若直接打印底层数组(假设获取到),会包含null Object[] arr = new Object[10]; arr[0] = 1; arr[1] = 2; arr[2] = 3; System.out.println(Arrays.toString(arr)); // 输出:[1, 2, 3, null, null, null, null, null, null, null],包含大量null } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/27 1:02:17

拒绝纸上谈兵:在“报错”中重塑 C++ 编译期与运行期思维

在 C 的世界里&#xff0c;错误是开发者最忠实的导师。许多初学者在遇到满屏的红色报错时往往感到焦虑&#xff0c;甚至试图通过盲目修改代码来“碰运气”消除错误。然而&#xff0c;真正的 C 高手都明白&#xff1a;无论是编译期错误还是运行期错误&#xff0c;它们都是程序在…

作者头像 李华
网站建设 2026/6/27 0:32:51

从Waring到DC分解:多项式凸表示的理论与算法实践

1. 从“和”到“差”&#xff1a;理解多项式凸表示的核心范式转换在优化、控制乃至机器学习领域&#xff0c;我们常常需要处理一个核心问题&#xff1a;如何将一个复杂的非线性函数&#xff0c;特别是多项式函数&#xff0c;表示成更容易处理的形式&#xff1f;一个直观的想法是…

作者头像 李华
网站建设 2026/6/27 0:27:16

告别等报表的日子:实时数据分析触手可及

核心观点&#xff1a; 实时数据分析一直被认为是高端技术活。Chat2DB连接ClickHouse等实时数仓&#xff0c;让业务人员用自然语言查实时数据&#xff0c;从T1进化到秒级响应&#xff0c;活动效果实时监控、异常即时发现。&#xfffd;&#xfffd; 前线战地报道记者&#xff1a…

作者头像 李华
网站建设 2026/6/27 0:25:15

SAI:解决Android拆分APK安装难题的模块化架构实现

SAI&#xff1a;解决Android拆分APK安装难题的模块化架构实现 【免费下载链接】SAI Android split APKs installer 项目地址: https://gitcode.com/gh_mirrors/sa/SAI 技术挑战与解决方案选择 Android应用分发模式从传统的单一APK文件演进到基于Android App Bundle的拆…

作者头像 李华
网站建设 2026/6/27 0:14:30

OBS Multi RTMP插件:免费开源的一键多平台直播终极解决方案

OBS Multi RTMP插件&#xff1a;免费开源的一键多平台直播终极解决方案 【免费下载链接】obs-multi-rtmp OBS複数サイト同時配信プラグイン 项目地址: https://gitcode.com/gh_mirrors/ob/obs-multi-rtmp 想要实现一键多平台直播吗&#xff1f;OBS Multi RTMP插件正是你…

作者头像 李华
网站建设 2026/6/27 0:07:59

企业级Pig系统安全加固实战:XSS立体防御与端到端数据加密

1. 项目概述&#xff1a;为什么Pig系统的安全防护值得你投入精力&#xff1f;如果你正在负责一个基于Pig框架&#xff08;这里指代一个常见的、用于快速构建后台管理系统的开源脚手架&#xff0c;而非Apache Pig大数据处理平台&#xff09;开发的企业级应用&#xff0c;那么“安…

作者头像 李华