【JDK优化】java.util.Arrays的排序研究

本文深入探讨了JDK中Arrays.sort方法使用的快速排序和归并排序算法,详细解析了这两种排序算法的优化策略,包括小规模数组直接插入排序、精心选择枢轴、优化枢轴划分等。

作者题记:JDK中有很多算法具有优化的闪光点,值得好好研究。

 

【java.uti.Arrays】 包含用来操作数组(比如排序和搜索)的各种方法。这篇文章我们就来研究一些大师们写的排序算法。


(1) 基本数据类型数组的排序,如Arrays.sort(int[])等。采用了一种经 调优的快速排序 该算法改编自 Jon L. Bentley 和 M. Douglas McIlroy 合著的 Engineering a Sort Function", Software-Practice and Experience Vol. 23(11) P. 1249-1265 (November 1993)。此算法在许多数据集上提供 n*log(n) 性能,这导致其他快速排序会降低二次型性能。

 

下面是JDK中调优快速排序算法的源代码:

   /**
     * 将指定范围的整形数组升序排序。
     * x[] 待排数组
     * off 从数组的第off个元素开始排序
     * len 数组长度
     */
    private static void sort1(int x[], int off, int len) {
	//优化1:在小规模(size<7)数组中,直接插入排序的效率要比快速排序高。
	if (len < 7) {
	    for (int i=off; i<len+off; i++)
	    	for (int j=i; j>off && x[j-1]>x[j]; j--)
	    		swap(x, j, j-1);
	    return;
	}

	//优化2:精心选择划分元素,即枢轴
	//如果是小规模数组(size<=7),直接取中间元素作为枢轴
	//如果是中等规模数组(7=<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数作为枢轴
	//如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)
	int m = off + (len >> 1);
	if (len > 7) { 
	    int l = off;
	    int n = off + len - 1;
	    if (len > 40) {        
			int s = len/8;
			l = med3(x, l, l+s, l+2*s);
			m = med3(x, m-s,   m,   m+s);
			n = med3(x, n-2*s, n-s, n);
	    }
	    m = med3(x, l, m, n);
	}
	int v = x[m];

         //优化3:每一次枢轴v的划分,都会形成形成一个形如  (<v)* v* (>v)*
        //阶段一,形成 v* (<v)* (>v)* v* 的数组
        int a = off, b = a, c = off + len - 1, d = c;
        while(true) {
	    while (b <= c && x[b] <= v) {
			if (x[b] == v)
			    swap(x, a++, b);
			b++;
	    }
	    while (c >= b && x[c] >= v) {
			if (x[c] == v)
			    swap(x, c, d--);
			c--;
	    }
	    if (b > c)
	    	break;
	    swap(x, b++, c--);
	}

	//阶段二,将枢轴和与枢轴相等的元素交换到数组中间
	int s, n = off + len;
	s = Math.min(a-off, b-a  );  vecswap(x, off, b-s, s);
	s = Math.min(d-c,   n-d-1);  vecswap(x, b,   n-s, s);

	//阶段三,递归排序与枢轴不相等都元素区间
	if ((s = b-a) > 1)
	    sort1(x, off, s);
	if ((s = d-c) > 1)
	    sort1(x, n-s, s);
    } 
 

优化1:在小规模(size<7)数组中,直接插入排序的效率要比快速排序高。

      没有一种排序在任何情况下都是最优的《基于比较的内部排序总结 》。 O(N^2)级别的排序看起来似乎比所有先进排序要差的多。但实际上也并非如此,Arrays中的sort()算法就给了我们一个很好的例子。当待排数组规模非常小的时候(JDK中规模的阈值为INSERTIONSORT_THRESHOLD=7),直接插入排序反而要比快排,归并排序要好。

           这个道理很简单。数组规模小,简单算法的比较次数不会比先进算法多多少。相反,诸如快排,归并排序等先进算法使用递归操作,所付出的运行代价更高。

 

优化2:精心选择划分元素,即枢轴。

      快排有一种最差的情况,即蜕化成效率最差的起跑排序(见《 交换排序 》)。 导致这种情况产生的主要原因就是枢轴的选择并不能把整个数组划分成两个大致相等的部分。比如对于基本有序的数组,选择第一个元素作为枢轴就会产生这种蜕化。

      既然如此,我们可以看看Arryas.sort()是如何为我们选择枢轴的。

      ● 如果是小规模数组(size<=7),直接取中间元素作为枢轴。      

      ● 如果是中等规模数组(7=<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数作为枢轴
      ● 如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)

      中小规模时,这种取法尽量可以避免数组的较小数或者较大数成为枢轴。值得一提的是大规模的时候,首先在数组中寻找9个数据(可以通过源代码发现这9个数据的位置较为平均的分布在整个数组上);然后每3个数据找中位数;最后在3个中位数上再找出一个中位数作为枢轴。

      仔细想想,这种精心选择的枢轴,使得快排的最差情况成为了极小概率事件了。

 

★ 优化3:根据枢轴v划分,形成一个形如  (<v)*   v* (>v)* 的数组

      普通快排算法,都是使得枢轴元素移动到数组的较中间位置。枢轴之前的元素全部小于或等于枢轴,之后的元素全部大于枢轴。但与枢轴相等的元素并不能移动到枢轴附近位置。这一点在Arrays.sort()算法中有很大的优化。

      我们举个例子来说明Arrays的优化细节       15、93、15、41、6、15、22、7、15、20

      第一次枢轴:v=15

      阶段一,形成 v* (<v)* (>v)* v* 的数组:

                                          15、15、 7、6、 41、20、22、93、 15、15

                  我们发现,与枢轴相等的元素都移动到了数组的两边。而比枢轴小的元素和比枢轴大的元素也都区分开来了。

      阶段二,将枢轴和与枢轴相等的元素交换到数组中间的位置上

                                          7、6、 15、15、 15、15、 41、20、22、93

      阶段三,递归排序与枢轴不相等都元素区间{7、6}和{41、20、22、93}

      仔细想想,对于重复元素较多的数组,这种优化无疑能到达更好的效率。

 

 

(1) 对象数组的排序,如Arrays.sort(Object[])等。采用了一种经 修改的归并排序 其也有几个优化的闪光点。

     下面是JDK中改进归并排序算法的源代码:

    /**
     * 将指定范围的对象数组按自然顺序升序排序。
     * src[] 原待排数组
     * dest[] 目的待排数组
     * low 待排数组的下界位置
     * high 待排数组的上界位置
     * off 从数组的第off个元素开始排序
     */    
    private static void mergeSort(Object[] src,
				  Object[] dest,
				  int low,
				  int high,
				  int off) {
	int length = high - low;

	//优化1:规模很小的数组的排序,直接插入排序的效率反而比归并要高。
	//规模定在INSERTIONSORT_THRESHOLD=7之内
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
			 ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }

        // 递归排序dest的一半元素并赋值给src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);

        //优化2:如果低子列表中的最高元素小于高子列表中的最低元素,则忽略合并        
        //如果需要归并的两端low~(middle-1),middle~high已经有序,即src[mid-1]==src[mid]。
        //那么只需要将src的low~high赋值对应的dest即可,无需再归并。
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }

        //将src的两个部分合并,并赋值给dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }
 

优化1: 同上面的快速排序


优化2: 如果低子列表中的最高元素小于高子列表中的最低元素,则忽略合并。 这个优化措施无疑对基本有序序列是极大的效率改进。

<think> 我们正在处理一个IndexOutOfBoundsException异常,具体错误信息是:Index 46 out of bounds for length 46。 这个错误表明我们试图访问一个数组或列表的第47个元素(因为索引从0开始),但该数组或列表只有46个元素(长度46),因此最大索引只能是45。 原因分析: 1. 索引计算错误:通常是因为循环条件或索引变量使用不当。 2. 边界条件处理不当:特别是在循环中,使用了`<=`(小于等于)而不是`<`(小于)作为条件,或者索引初始值设置错误。 3. 数组/列表长度与索引的对应关系:索引范围是0到length-1,而错误中索引等于长度(46),这超出了有效范围(0到45)。 从引用[3]中可以看到一个类似的例子:Index 3 out of bounds for length 3。这是因为循环条件写成了`i<=arrayList.size()`,而正确的应该是`i<arrayList.size()`,因为`size()`返回的是元素个数,最大索引为`size()-1`。 因此,这个错误通常发生在以下情况: - 循环条件错误:例如`for (int i=0; i<=list.size(); i++)`,应该改为`for (int i=0; i<list.size(); i++)`。 - 试图访问最后一个元素的下一个元素:例如,在循环结束后仍然使用索引`list.size()`。 - 索引变量在其他地方被错误地增加(如使用了递增操作但没有检查边界)。 如何解决? 1. 检查代码中访问数组或列表的索引位置,特别是循环条件。 2. 确保索引值在0到length-1的范围内。 3. 使用调试工具或打印索引值来定位问题。 示例代码错误: 假设有一个长度为46的列表,我们试图访问索引46(即第47个元素)就会抛出此异常。 例如: List<String> list = new ArrayList<>(46); // 假设这个列表有46个元素 String s = list.get(46); // 这将抛出IndexOutOfBoundsException,因为合法索引是0-45 或者循环错误: for (int i=0; i<=list.size(); i++) { // 当i等于list.size()时,list.get(i)就会越界 list.get(i); } 修正后: for (int i=0; i<list.size(); i++) { list.get(i); } 另外,引用[1][2][4][5]都展示了类似错误,都是因为索引超出了数组或列表的有效范围。 因此,针对错误“Index 46 out of bounds for length 46”,我们需要检查代码中是否有索引等于46的地方(因为长度为46,最大索引应为45)。重点检查循环条件和直接使用46作为索引的地方。 可能的原因还包括: - 计算索引的公式有误,例如使用`list.size()`作为索引,而应该用`list.size()-1`。 - 在迭代过程中,索引变量被错误地增加了多次。 建议: - 使用IDE的调试功能,查看索引变量在运行时的值。 - 在访问数组或列表前,检查索引是否合法:`if (index >=0 && index < array.length) { ... }`。 根据引用[3]的案例,错误代码是: for(int i=0;i<=arrayList.size();i++) { ... } 修正为: for(int i=0;i<arrayList.size();i++) { ... } 所以,检查你的代码中所有使用索引46(或等于列表长度的索引)的地方,确保索引值在0到45之间。 </think> ### 索引越界错误分析:`Index 46 out of bounds for length 46` 这个异常表示程序试图访问容器中**第47个元素**(索引从0开始),但容器实际长度只有46个元素。错误本质是**索引值等于容器长度**,而合法索引范围是 `0` 到 `length-1`(即0到45)。 #### ⚠️ 核心原因分析 ```java // 错误示例:试图访问索引46(第47个元素) container.get(46); // 但容器长度只有46(最大有效索引=45) ``` ##### 常见触发场景: 1. **循环边界错误**(最常见) 循环条件使用 `<= length` 而非 `< length`: ```java // 错误写法:当 i=length 时会越界 for (int i = 0; i <= array.length; i++) { array[i] = ... // 最后一次循环 i=46 触发异常 [^3] } ``` 2. **索引计算错误** ```java int index = list.size(); // 错误:index=46 已是越界位置 String value = list.get(index); // 触发异常 ``` 3. **集合操作后的同步问题** ```java List<String> data = new ArrayList<>(...); // 初始长度46 data.remove(0); // 删除后长度变为45 System.out.println(data.get(45)); // 仍尝试访问原索引45(现最大索引=44) ``` #### ✅ 解决方案 1. **修正循环条件** 将 `<= length` 改为 **`< length`**: ```java // 正确写法 for (int i = 0; i < array.length; i++) { // 安全访问 0~45 } ``` 2. **动态检查索引有效性** ```java int targetIndex = 46; if (targetIndex >= 0 && targetIndex < list.size()) { list.get(targetIndex); } else { System.out.println("索引越界: " + targetIndex); } ``` 3. **使用迭代器避免索引计算** ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { String item = it.next(); // 无需索引 } ``` #### 📊 索引边界对照表 | 操作 | 合法索引范围 | 越界典型案例 | 后果 | |----------------------|--------------------|-------------------|--------------------| | 访问元素 (`get(i)`) | `0 ≤ i ≤ length-1` | `i = length` | `IndexOutOfBounds` | | 删除元素 (`remove(i)`) | `0 ≤ i ≤ length-1` | `i = length` | 同上 | | 循环终止条件 | `i < length` | `i <= length` | 同上 [^3] | > 关键规律:**当索引值等于容器长度时必定越界**,因为容器索引始终从0开始计数。 #### 🔍 调试建议 1. 在异常发生位置打印容器长度: ```java System.out.println("容器长度=" + list.size()); // 确认实际长度 ``` 2. 检查索引计算逻辑中的**边界值处理** 3. 使用调试器观察索引变量在循环中的变化过程 --- ### 相关问题 1. 为什么Java集合的索引从0开始而不是1?这种设计有哪些优缺点?[^3] 2. 如何避免在多线程环境下因集合动态修改导致的`IndexOutOfBoundsException`? 3. `Arrays.asList()`和`new ArrayList<>()`在索引越界行为上有区别吗? 4. 为什么`IndexOutOfBoundsException`是`RuntimeException`(非受检异常)?[^3][^5]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值