zoukankan      html  css  js  c++  java
  • Java编程的逻辑 (31)

    数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高。

    数组操作是计算机程序中的常见基本操作,Java中有一个类Arrays,包含一些对数组操作的静态方法,本节主要就来讨论这些方法,我们先来看怎么用,然后再来看它们的实现原理。学习Arrays的用法,我们就可以避免重新发明轮子,直接使用,学习它的实现原理,我们就可以在需要的时候,自己实现它不具备的功能。

    用法

    toString

    Arrays的toString方法可以方便的输出一个数组的字符串形式,方便查看,它有九个重载的方法,包括八种基本类型数组和一个对象类型数组,这里列举两个:

    public static String toString(int[] a)
    public static String toString(Object[] a) 

    例如:

    int[] arr = {9,8,3,4};
    System.out.println(Arrays.toString(arr));
    
    String[] strArr = {"hello", "world"};
    System.out.println(Arrays.toString(strArr));

    输出为

    [9, 8, 3, 4]
    [hello, world]

    如果不使用Arrays.toString,直接输出数组自身,即代码改为:

    int[] arr = {9,8,3,4};
    System.out.println(arr);
    
    String[] strArr = {"hello", "world"};
    System.out.println(strArr);

    则输出会变为如下所示:

    [I@1224b90
    [Ljava.lang.String;@728edb84

    这个输出就难以阅读了,@后面的数字表示的是内存的地址。

    数组排序 - 基本类型

    排序是一个非常常见的操作,同toString一样,对每种基本类型的数组,Arrays都有sort方法(boolean除外),如:

    public static void sort(int[] a)
    public static void sort(double[] a)

    排序按照从小到大升序排,看个例子:

    int[] arr = {4, 9, 3, 6, 10};
    Arrays.sort(arr);
    System.out.println(Arrays.toString(arr));

    输出为:

    [3, 4, 6, 9, 10]

    数组已经排好序了。

    sort还可以接受两个参数,对指定范围内的元素进行排序,如:

    public static void sort(int[] a, int fromIndex, int toIndex)

    包括fromIndex位置的元素,不包括toIndex位置的元素,如:

    int[] arr = {4, 9, 3, 6, 10};
    Arrays.sort(arr,0,3);
    System.out.println(Arrays.toString(arr));

    输出为:

    [3, 4, 9, 6, 10]

    只对前三个元素排序。

    数组排序 - 对象类型

    除了基本类型,sort还可以直接接受对象类型,但对象需要实现Comparable接口。

    public static void sort(Object[] a)
    public static void sort(Object[] a, int fromIndex, int toIndex) 

    我们看个String数组的例子:

    String[] arr = {"hello","world", "Break","abc"};
    Arrays.sort(arr);
    System.out.println(Arrays.toString(arr));

    输出为:

    [Break, abc, hello, world]

    "Break"之所以排在最前面,是因为大写字母比小写字母都小。那如果排序的时候希望忽略大小写呢?

    数组排序 - 自定义比较器

    sort还有另外两个重载方法,可以接受一个比较器作为参数:

    public static <T> void sort(T[] a, Comparator<? super T> c)
    public static <T> void sort(T[] a, int fromIndex, int toIndex,
                                    Comparator<? super T> c)

    方法声明中的T表示泛型,泛型我们在后续章节再介绍,这里表示的是,这个方法可以支持所有对象类型,只要传递这个类型对应的比较器就可以了。Comparator就是比较器,它是一个接口,定义是:

    public interface Comparator<T> {
        int compare(T o1, T o2);
        boolean equals(Object obj);
    }

    最主要的是compare这个方法,它比较两个对象,返回一个表示比较结果的值,-1表示o1小于o2,0表示相等,1表示o1大于o2。

    排序是通过比较来实现的,sort方法在排序的过程中,需要对对象进行比较的时候,就调用比较器的compare方法。

    String类有一个public静态成员,表示忽略大小写的比较器:

    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();

    我们通过这个比较器再来对上面的String数组排序:

    String[] arr = {"hello","world", "Break","abc"};
    Arrays.sort(arr, String.CASE_INSENSITIVE_ORDER);
    System.out.println(Arrays.toString(arr));

    这样,大小写就忽略了,输出变为了:

    [abc, Break, hello, world]

    为进一步理解Comparator,我们来看下String的这个比较器的主要实现代码:

    复制代码
    private static class CaseInsensitiveComparator
            implements Comparator<String> {
        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }
    }
    复制代码

    代码比较直接,就不解释了。

    sort默认都是从小到大排序,如果希望按照从大到小排呢?对于对象类型,可以指定一个不同的Comparator,可以用匿名内部类来实现Comparator,比如可以这样:

    复制代码
    String[] arr = {"hello","world", "Break","abc"};
    Arrays.sort(arr, new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return o2.compareToIgnoreCase(o1);
        }
    });
    System.out.println(Arrays.toString(arr));
    复制代码

    程序输出为:

    [world, hello, Break, abc]

    以上代码使用一个匿名内部类实现Comparator接口,返回o2与o1进行忽略大小写比较的结果,这样就能实现,忽略大小写,且按从大到小排序。为什么o2与o1比就逆序了呢?因为默认情况下,是o1与o2比。

    Collections类中有两个静态方法,可以返回逆序的Comparator,如

    public static <T> Comparator<T> reverseOrder()
    public static <T> Comparator<T> reverseOrder(Comparator<T> cmp)

    关于Collections类,我们在后续章节再详细介绍。

    这样,上面字符串忽略大小写逆序排序的代码可以改为:

    String[] arr = {"hello","world", "Break","abc"};
    Arrays.sort(arr, Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
    System.out.println(Arrays.toString(arr));

    传递比较器Comparator给sort方法,体现了程序设计中一种重要的思维方式,将不变和变化相分离,排序的基本步骤和算法是不变的,但按什么排序是变化的,sort方法将不变的算法设计为主体逻辑,而将变化的排序方式设计为参数,允许调用者动态指定,这也是一种常见的设计模式,它有一个名字,叫策略模式,不同的排序方式就是不同的策略。

    二分查找

    Arrays包含很多与sort对应的查找方法,可以在已排序的数组中进行二分查找,所谓二分查找就是从中间开始找,如果小于中间元素,则在前半部分找,否则在后半部分找,每比较一次,要么找到,要么将查找范围缩小一半,所以查找效率非常高。

    二分查找既可以针对基本类型数组,也可以针对对象数组,对对象数组,也可以传递Comparator,也都可以指定查找范围,如下所示:

    针对int数组

    public static int binarySearch(int[] a, int key)
    public static int binarySearch(int[] a, int fromIndex, int toIndex,
                                           int key)

    针对对象数组

    public static int binarySearch(Object[] a, Object key)

    自定义比较器

    public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c) 

    如果能找到,binarySearch返回找到的元素索引,比如说:

    int[] arr = {3,5,7,13,21};
    System.out.println(Arrays.binarySearch(arr, 13));

    输出为3。如果没找到,返回一个负数,这个负数等于:-(插入点+1),插入点表示,如果在这个位置插入没找到的元素,可以保持原数组有序,比如说:

    int[] arr = {3,5,7,13,21};
    System.out.println(Arrays.binarySearch(arr, 11));

    输出为-4,表示插入点为3,如果在3这个索引位置处插入11,可以保持数组有序,即数组会变为:{3,5,7,11,13,21}

    需要注意的是,binarySearch针对的必须是已排序数组,如果指定了Comparator,需要和排序时指定的Comparator保持一致,另外,如果数组中有多个匹配的元素,则返回哪一个是不确定的。

    数组拷贝

    与toString一样,也有多种重载形式,如:

    public static long[] copyOf(long[] original, int newLength)
    public static <T> T[] copyOf(T[] original, int newLength)

    后面那个是泛型用法,这里表示的是,这个方法可以支持所有对象类型,参数是什么数组类型,返回结果就是什么数组类型。

    newLength表示新数组的长度,如果大于原数组,则后面的元素值设为默认值。回顾一下默认值,对于数值类型,值为0,对于boolean,值为false,对于char,值为'',对于对象,值为null。

    来看个例子:

    String[] arr = {"hello", "world"};
    String[] newArr = Arrays.copyOf(arr, 3);
    System.out.println(Arrays.toString(newArr));

    输出为:

    [hello, world, null]

    除了copyOf方法,Arrays中还有copyOfRange方法,以支持拷贝指定范围的元素,如:

    public static int[] copyOfRange(int[] original, int from, int to)

    from表示要拷贝的第一个元素的索引,新数组的长度为to-from,to可以大于原数组的长度,如果大于,与copyOf类似,多出的位置设为默认值。

    来看个例子:

    int[] arr = {0,1,3,5,7,13,19};
    int[] subArr1 = Arrays.copyOfRange(arr,2,5);
    int[] subArr2 = Arrays.copyOfRange(arr,5,10);
    System.out.println(Arrays.toString(subArr1));
    System.out.println(Arrays.toString(subArr2));

    输出为:

    [3, 5, 7]
    [13, 19, 0, 0, 0]

    subArr1是正常的子数组,subArr2拷贝时to大于原数组长度,后面的值设为了0。

    数组比较

    支持基本类型和对象类型,如下所示:

    public static boolean equals(boolean[] a, boolean[] a2)
    public static boolean equals(double[] a, double[] a2)
    public static boolean equals(Object[] a, Object[] a2)

    只有数组长度相同,且每个元素都相同,才返回true,否则返回false。对于对象,相同是指equals返回true。

    填充值

    Arrays包含很多fill方法,可以给数组中的每个元素设置一个相同的值:

    public static void fill(int[] a, int val)

    也可以给数组中一个给定范围的每个元素设置一个相同的值:

    public static void fill(int[] a, int fromIndex, int toIndex, int val)

    比如说:

    int[] arr = {3,5,7,13,21};
    Arrays.fill(arr,2,4,0);
    System.out.println(Arrays.toString(arr));

    将索引从2(含2)到4(不含4)的元素设置为0,输出为:

    [3, 5, 0, 0, 21]

    哈希值

    针对数组,计算一个数组的哈希值:

    public static int hashCode(int a[]) 

    计算hashCode的算法和String是类似的,我们看下代码:

    复制代码
    public static int hashCode(int a[]) {
        if (a == null)
            return 0;
    
        int result = 1;
        for (int element : a)
            result = 31 * result + element;
    
        return result;
    }
    复制代码

    回顾一下,String计算hashCode的算法也是类似的,数组中的每个元素都影响hash值,位置不同,影响也不同,使用31一方面产生的哈希值更分散,另一方面计算效率也比较高。

    多维数组

    之前我们介绍的数组都是一维的,数组还可以是多维的,先来看二维数组,比如:

    复制代码
    int[][] arr = new int[2][3];
    for(int i=0;i<arr.length;i++){
        for(int j=0;j<arr[i].length;j++){
            arr[i][j] = i+j;
        }
    }
    复制代码

    arr就是一个二维数组,第一维长度为2,第二维长度为3,类似于一个长方形矩阵,或者类似于一个表格,第一维表示行,第二维表示列。arr[i]表示第i行,它本身还是一个数组,arr[i][j]表示第i行中的第j个元素。

    除了二维,数组还可以是三维、四维等,但一般而言,很少用到三维以上的数组,有几维,就有几个[],比如说,一个三维数组的声明为:

    int[][][] arr = new int[10][10][10];

    在创建数组时,除了第一维的长度需要指定外,其他维的长度不需要指定,甚至,第一维中,每个元素的第二维的长度可以不一样,看个例子:

    int[][] arr = new int[2][];
    arr[0] = new int[3];
    arr[1] = new int[5];

    arr是一个二维数组,第一维的长度为2,第一个元素的第二维长度为3,而第二个为5。

    多维数组到底是什么呢?其实,可以认为,多维数组只是一个假象,只有一维数组,只是数组中的每个元素还可以是一个数组,这样就形成二维数组,如果其中每个元素还都是一个数组,那就是三维数组。

    多维数组的操作

    Arrays中的toString,equals,hashCode都有对应的针对多维数组的方法:

    public static String deepToString(Object[] a)
    public static boolean deepEquals(Object[] a1, Object[] a2)
    public static int deepHashCode(Object a[])

    这些deepXXX方法,都会判断参数中的元素是否也为数组,如果是,会递归进行操作。

    看个例子:

    复制代码
    int[][] arr = new int[][]{
            {0,1},
            {2,3,4},
            {5,6,7,8}
    };
    System.out.println(Arrays.deepToString(arr));
    复制代码

    输出为:

    [[0, 1], [2, 3, 4], [5, 6, 7, 8]]

    实现原理

    hashCode的实现我们已经介绍了,fill和equals的实现都很简单,循环操作而已,就不赘述了。

    toString

    toString的实现也很简单,利用了StringBuilder,我们列下代码,但不做解释了。

    复制代码
    public static String toString(int[] a) {
        if (a == null)
            return "null";
        int iMax = a.length - 1;
        if (iMax == -1)
            return "[]";
    
        StringBuilder b = new StringBuilder();
        b.append('[');
        for (int i = 0; ; i++) {
            b.append(a[i]);
            if (i == iMax)
                return b.append(']').toString();
            b.append(", ");
        }
    }
    复制代码

    拷贝

    copyOf和copyOfRange利用了 System.arraycopy,逻辑也很简单,我们也只是简单列下代码:

    复制代码
    public static int[] copyOfRange(int[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        int[] copy = new int[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    } 
    复制代码

    二分查找

    二分查找binarySearch的代码也比较直接,主要代码如下:

    复制代码
    private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
                                         T key, Comparator<? super T> c) {
        int low = fromIndex;
        int high = toIndex - 1;
    
        while (low <= high) {
            int mid = (low + high) >>> 1;
            T midVal = a[mid];
            int cmp = c.compare(midVal, key);
            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }
    复制代码

    有两个标志low和high,表示查找范围,在while循环中,与中间值进行对比,大于则在后半部分找(提高low),否则在前半部分找(降低high)。

    排序

    最后,我们来看排序方法sort,与前面这些简单直接的方法相比,sort要复杂的多,排序是计算机程序中一个非常重要的方面,几十年来,计算机科学家和工程师们对此进行了大量的研究,设计实现了各种各样的算法和实现,进行了大量的优化。一般而言,没有一个所谓最好的算法,不同算法往往有不同的适用场合。

    那Arrays的sort是如何实现的呢?

    对于基本类型的数组,Java采用的算法是双枢轴快速排序(Dual-Pivot Quicksort),这个算法是Java 1.7引入的,在此之前,Java采用的算法是普通的快速排序,双枢轴快速排序是对快速排序的优化,新算法的实现代码位于类java.util.DualPivotQuicksort中。

    对于对象类型,Java采用的算法是TimSort, TimSort也是在Java 1.7引入的,在此之前,Java采用的是归并排序,TimSort实际上是对归并排序的一系列优化,TimSort的实现代码位于类java.util.TimSort中。

    在这些排序算法中,如果数组长度比较小,它们还会采用效率更高的插入排序。

    为什么基本类型和对象类型的算法不一样呢?排序算法有一个稳定性的概念,所谓稳定性就是对值相同的元素,如果排序前和排序后,算法可以保证它们的相对顺序不变,那算法就是稳定的,否则就是不稳定的。

    快速排序更快,但不稳定,而归并排序是稳定的。对于基本类型,值相同就是完全相同,所以稳定不稳定没有关系。但对于对象类型,相同只是比较结果一样,它们还是不同的对象,其他实例变量也不见得一样,稳定不稳定可能就很有关系了,所以采用归并排序。

    这些算法的实现是比较复杂的,所幸的是,Java给我们提供了很好的实现,绝大多数情况下,我们会用就可以了。

    更多方法

    其实,Arrays中包含的数组方法是比较少的,很多常用的操作没有,比如,Arrays的binarySearch只能针对已排序数组进行查找,那没有排序的数组怎么方便查找呢?

    Apache有一个开源包(http://commons.apache.org/proper/commons-lang/),里面有一个类ArrayUtils (位于包org.apache.commons.lang3),里面实现了更多的常用数组操作,这里列举一些,与Arrays类似,每个操作都有很多重载方法,我们只列举一个。

    翻转数组元素

    public static void reverse(final int[] array)

    对于基本类型数组,Arrays的sort只能从小到大排,如果希望从大到小,可以在排序后,使用reverse进行翻转。

    查找元素

    复制代码
    //从头往后找
    public static int indexOf(final int[] array, final int valueToFind)
    
    //从尾部往前找
    public static int lastIndexOf(final int[] array, final int valueToFind)
    
    //检查是否包含元素
    public static boolean contains(final int[] array, final int valueToFind)
    复制代码

    删除元素

    因为数组长度是固定的,删除是通过创建新数组,然后拷贝除删除元素外的其他元素来实现的。

    复制代码
    //删除指定位置的元素
    public static int[] remove(final int[] array, final int index)
    
    //删除多个指定位置的元素
    public static int[] removeAll(final int[] array, final int... indices)
    
    //删除值为element的元素,只删除第一个
    public static boolean[] removeElement(final boolean[] array, final boolean element) 
    复制代码

    添加元素

    同删除一样,因为数组长度是固定的,添加是通过创建新数组,然后拷贝原数组内容和新元素来实现的。

    复制代码
    //添加一个元素
    public static int[] add(final int[] array, final int element)
    
    //在指定位置添加一个元素
    public static int[] add(final int[] array, final int index, final int element)
    
    //合并两个数组
    public static int[] addAll(final int[] array1, final int... array2) 
    复制代码

    判断数组是否是已排序的

    public static boolean isSorted(int[] array) 

    小结

    本节我们分析了Arrays类,介绍了其用法,以及基本实现原理,同时,我们介绍了多维数组以及Apache中的ArrayUtils类。对于带Comparator参数的排序方法,我们提到,这是一种思维和设计模式,值得学习。

    数组是计算机程序中的基本数据结构,Arrays类以及ArrayUtils类封装了关于数组的常见操作,使用这些方法吧!

    下一节,我们来看计算机程序中,另一种常见的操作,就是对日期的操作。

  • 相关阅读:
    [APM] OneAPM 云监控部署与试用体验
    Elastic Stack 安装
    xBIM 综合使用案例与 ASP.NET MVC 集成(一)
    JQuery DataTables Selected Row
    力导向图Demo
    WPF ViewModelLocator
    Syncfusion SfDataGrid 导出Excel
    HTML Table to Json
    .net core 2.0 虚拟目录下载 Android Apk 等文件
    在BootStrap的modal中使用Select2
  • 原文地址:https://www.cnblogs.com/ivy-xu/p/12388819.html
Copyright © 2011-2022 走看看