前言

数组(Array)是一种线性表数据结构,利用一组连续的内存空间,存储一组具有相同类型的数据。

概念介绍

首先我们说一下什么是线性表,线性表就是数据排成一条线的数据结构,每个线性表最多只有前和后两个方向,数组、链表、队列、栈等都是线性表结构。那么什么是非线性表呢?二叉树、堆、图等数据结构就是非线性表,在非线性表中数据之间并不是简单的前后关系。

其次,数组的内存空间是连续的,数据类型也是相同的,正是因为这两个特性,数组的随机访问速度非常快。我们来看下数组是怎么进行随机访问的,假定我们有一个长度为10的int类型的数组int[] a = new int[10],计算机给该数组分配的内存空间为100~110,其中内存块的首地址base_address=100。当计算机随机访问数组中的某个元素时,会先通过寻址公式a[i]_address = base_address + i * data_type_size计算出该元素的内存地址,其中data_type_size代表数组中每个元素的大小,我们的数组是int类型的,所以每个元素就是4个字节。这样计算出元素的地址后就立马找到该元素了。

面试的时候我们经常被问到数组和链表的区别,有时候我们会回答“链表适合插入、删除,时间复杂度是O(1);数组适合查找,查找的时间复杂度是O(1)”,其实这种描述是不准确的,数组适合查找这是没问题的,但是时间复杂度不是O(1),即便是用二分查找对排好序的数组进行查找,时间复杂度也是O(Logn),所以,正确的表述应该是“数组支持随机访问,根据下标随机访问的时间复杂度是O(1)”。

数组声明

数组声明有两种方式:

  1. 数据类型 [] 数组名称 = new 数据类型[数组长度];
  2. 数据类型 [] 数组名称 = {数组元素1,数组元素2,......}

数组实现

我们知道,一个数组需要具备如下功能:

  • 插入数据
  • 查找数据
  • 删除数据
  • 迭代数据

下边,我们实现一个自己的数组结构:

public class MyArray { // 定义一个数组 private int[] intArray; // 定义数组的实际有效长度 private int elems; // 定义数组的最大长度 private int length; // 默认构造一个长度为50的数组 public MyArray() { elems = 0; length = 50; intArray = new int[length]; } // 构造函数,初始化一个长度为length 的数组 public MyArray(int length) { elems = 0; this.length = length; intArray = new int[length]; } // 获取数组的有效长度 public int getSize() { return elems; } /**  * 遍历显示元素  */ public void display() { for (int i = 0; i < elems; i++) { System.out.print(intArray[i] + " "); } System.out.println(); } /**  * 添加元素  *   * @param value,假设操作人是不会添加重复元素的,如果有重复元素对于后面的操作都会有影响。  * @return 添加成功返回true,添加的元素超过范围了返回false  */ public boolean add(int value) { if (elems == length) { return false; } else { intArray[elems] = value; elems++; } return true; } /**  * 根据下标获取元素  *   * @param i * @return 查找下标值在数组下标有效范围内,返回下标所表示的元素 查找下标超出数组下标有效值,提示访问下标越界 */public int get(int i) {if (i < 0 || i > elems) {System.out.println("访问下标越界");}return intArray[i];}/** * 查找元素 *  * @param searchValue * @return 查找的元素如果存在则返回下标值,如果不存在,返回 -1 */public int find(int searchValue) {int i;for (i = 0; i < elems; i++) {if (intArray[i] == searchValue) {break;}}if (i == elems) {return -1;}return i;}/** * 删除元素 *  * @param value * @return 如果要删除的值不存在,直接返回 false;否则返回true,删除成功 */public boolean delete(int value) {int k = find(value);if (k == -1) {return false;} else {if (k == elems - 1) {elems--;} else {for (int i = k; i < elems - 1; i++) {intArray[i] = intArray[i + 1];}elems--;}return true;}}/** * 修改数据 *  * @param oldValue原值 * @param newValue新值 * @return 修改成功返回true,修改失败返回false */public boolean modify(int oldValue, int newValue) {int i = find(oldValue);if (i == -1) {System.out.println("需要修改的数据不存在");return false;} else {intArray[i] = newValue;return true;}}}

插入数据

前面我们说了,数组的插入和删除操作效率特别低,这是因为内存空间是连续的,为了保证内存空间的连续性,在插入和删除时会做很多搬移数据的操作。比如,我们有一个长度为n的数组,现在要将一个数据插入到数组的第k个位置,为了把这个位置腾出来给新来的数据,我们需要将第k~n这部分的元素顺序的往后挪一位,如下代码所示:

public static int[] insertVal(int[] arr, int insertIndex, int insertVal){ if(insertIndex < 0 || insertIndex > arr.length){ throw new IllegalArgumentException("插入位置错误"); } int[] tmpArr = Arrays.copyOf(arr, arr.length + 1); // 将insertIndex后边的元素一次挪动一位,给新元素腾空,从最后一个元素开始挪 for (int i = tmpArr.length - 1; i > insertIndex; i--) { tmpArr[i] = tmpArr[i - 1]; } tmpArr[insertIndex] = insertVal; return tmpArr;}

如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是O(n)。因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法搬移k之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数组插入到第k个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第k位的数据搬移到数组元素的最后,把新的元素直接放入第k个位置。如下代码所示:

public static int[] insertVal(int[] arr, int insertIndex, int insertVal){ if(insertIndex < 0 || insertIndex > arr.length){ throw new IllegalArgumentException("插入位置错误"); } int[] tmpArr = Arrays.copyOf(arr, arr.length + 1); if(insertIndex == arr.length){ // 插入到最后 tmpArr[insertIndex] = insertVal; } else { tmpArr[arr.length] = arr[insertIndex]; tmpArr[insertIndex] = insertVal; } return tmpArr;}

利用这种方式,在特定场景下,在第k个位置插入一个元素的时间复杂度就会降为O(1),快速排序算法就是这么干的。

删除数据

和上面的插入数据一样,如果我们要删除第k个位置的数据,为了保证内存的连续性,第k之后的数据都要往前挪一位,和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为O(1);如果删除开头的数据,则最坏情况时间复杂度为O(n);平均情况时间复杂度也为 O(n)。如下代码所示:

public static int[] delete(int[] arr, int index) {// 判断是否合法 if (index >= arr.length || index < 0) { throw new IllegalArgumentException("位置错误"); } int[] res = new int[arr.length - 1]; for (int i = 0; i < res.length; i++) { if (i < index) { res[i] = arr[i]; } else { res[i] = arr[i + 1]; } }return res;}

实际上,在某些特殊场景下,我们可以将多次删除操作合并执行,例如数组a[8]中存储了8个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c三个元素。为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

以上其实就是JVM的标记清除算法的实现原理(大多数主流虚拟机采用可达性分析算法来判断对象是否存活,在标记阶段,会遍历所有 GC ROOTS(根对象),将所有GC ROOTS可达的对象标记为存活。只有当标记工作完成后,清理工作才会开始。不足:1.效率问题:标记和清理效率都不高,但是当知道只有少量垃圾产生时会很高效。2.空间问题:会产生不连续的内存空间碎片。)

数组越界问题

在C语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。所以在C语言中即便数据访问越界,程序依然是可以执行的,只是这时候程序会出现莫名其妙的执行结果,数组越界在C语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。但是高级语言,如java语言是自带检查机制的,如果访问数据越界会报java.lang.ArrayIndexOutOfBoundsException错误。

容器类与数组的使用场景

现在很多的编程语言中都提供了容器类,如java语言中的ArrayList,那么在进行开发的时候,什么时候用容器类,什么时候用数组呢?还是以java中的ArrayList为例,这也是我用的最多的容器类,它最大的优势就是使用方便,已经封装了一系列的操作,而且不用手动为其扩容,ArrayList支持动态扩容。

数组定义的时候需要预先指定大小,进而分配连续的存储空间。如果我们定义的数组大小是10,这时候来了第11个数组元素,我们需要重新分配一块更大的存储空间,将原来的数组复制过去(java中已经封装了工具类System.arraycopy和Arrays.copyOf),然后将新的数据插入。如果使用ArrayList,我们就不需要关心底层的扩容逻辑,ArrayList已经帮我们实现好了,每次空间不够的时候,它就会将空间自动扩容为1.5倍大小,如下为ArrayList中扩容的代码:

/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = **Arrays.copyOf**(elementData, newCapacity);}

由以上ArrayList的源码可以看出,其实其内部在扩容时也是封装了数组的拷贝Arrays.copyOfoldCapacity >> 1右移一位操作,如果该数为正,则高位补0,若为负数,则高位补1,说白了就是除以2。由此可以看出新的列表是老的列表的1.5倍。

不过因为扩容涉及到内存申请和数据搬移,是比较耗时的,所以,如果我们事先能确定需要存储的数据大小,最好在创建ArrayList的时候就事先指定数据大小。以下代码为ArrayList的两种创建方式:

/** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};............/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); }}/** * Constructs an empty list with an initial capacity of ten. */public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

可以看出,如果不指定大小,ArrayList默认就是一个空的对象。在添加元素时,该对象会将大小设置为10,下面为ArrayList的源码:

/** * Default initial capacity. */private static final int **DEFAULT_CAPACITY** = 10;............private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return **Math.max(DEFAULT_CAPACITY, minCapacity);** } return minCapacity;}

言归正传,我们接着说数组,ArrayList这一类的集合类已经这么强大了,我们还要数组干什么呢?

其实很多时候,用数组比用ArrayList这一类的集合类更合适:

  • 1. 比如int、long这一类的基础数据类型,如果用ArrayList存储,则需要进行装箱操作,将其封装为Integer、Long类,装箱拆箱操作时需要时间的,有一定性能消耗。所以这时候就可以选择数组。
  • 2. 如果事先知道数据大小,并且集合类中的大部分方法用不到,操作非常简单的话就可以用数组。
  • 3. 在表示多维数组时,用数组会更加直观,如果用集合类,则需要进行嵌套。

当然,其实很多时候我们没必要过于追求性能,损耗一丢丢的性能,大部分情况下对系统整体性能没有什么影响,集合类已经帮我们实现了很多的操作,用起来是非常方便的。但是如果是做底层开发,性能就必须做到极致,这时候优先选择数组。

二维数组

对于 m * n 的数组,m表示这个二维数组有多少个一维数组,表示每一个一维数组的元素有多少个。元素 a[i][j] (i

  • address = base_address + ( i * n + j) * type_size

二维数组在进行内存分配时,必须知道其一维数组的大小,首先给一个地址值给数组a,然后开始为二位数组的一维数组部分进行分配空间,如果在定义二维数组时,并没有告诉其二维数组部分的大小,如:数据类型[][] 数组名 = new 数据类型[m][]这时候就无法为其一维数组分配静态的内存空间,这时候打印其地址值都是null,但是可以动态的分配空间。

下标之谜

现在我们思考一个问题,数组的下标为什么从0开始,按照人的思维逻辑,从1开始应该是更合理才是?

从数组存储数据的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”,也就是元素距离数组首地址的偏移量。a[0]也就是偏移量为0的位置,也就是首地址,a[k]表示偏移k个元素类型长度的位置,所以a[k]的内存地址计算公式为:

a[k]_address = base_address + k * type_size

但是,如果数组从1开始计数呢,那计算a[k]的内存地址公式就变为:

a[k]_address = base_address + (k-1)*type_size

对比以上两个计算公式,我们会发现,如果数组下标从1开始,每次随机访问数组元素时都多了一次减法运算,对于CPU来说,就多了一次减法指令。数组值得称赞的地方就是通过下标随机访问元素的速度,而通过下标随机访问数组元素又是非常基础的编程操作,效率的优化自然要做到极致。为了减少一次减法操作,数组选择从0开始编号也就是理所当然了。当然还有一方面原因,就是C语言中的数组下标从0开始,其他语言都是在C语言之后出现的,为了减少学习学习成本,尽量模仿C语言中的语法因此也继续用0开始做下标。

数组常用操作

排序

  • 直接排序
public static void sort(int[] arr) { for (int x = 0; x < arr.length - 1; x++) { for (int y = x + 1; y < arr.length; y++) { if (arr[x] > arr[y]) { int temp = arr[x]; arr[x] = arr[y]; arr[y] = temp; } } }}
  • 冒泡排序
public static void sort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { boolean f = true;// 每一轮都定义一个开关 // 每次内循环的比较,从0索引开始,每次都在递减。注意内循环的次数应该是(arr.length - 1 - i)。 for (int j = 0; j < arr.length - 1 - i; j++) { // 比较的索引是j和j+1 if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; f = false;// 发生交换,修改开关的状态 } } // 此轮结束,查看开关的状态 if (f) { // 开关状态没变,说明已经完成了排序 // 所以,不用继续下一轮了。 break; } }}
  • 比较排序(选择排序)
public static void sort(int[] arr) { // 外层循环控制的是比较的轮数:元素的个数-1 for (int i = 0; i < arr.length - 1; i++) { // 内层控制的是两两比较的次数 for (int j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } }}

上面这种选择排序方式可以优化为下面的方式:

public static void sort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { int index = i; int value = arr[i]; for (int j = i + 1; j < arr.length; j++) { if (arr[j] < value) { index = j; value = arr[j]; } } // 判断,是否有必要交换两个元素 if (index != i) { int tmp = arr[i]; arr[i] = arr[index]; arr[index] = tmp; } }}

这样可以减少很多数据交换次数。

  • 插入排序
public static void sort(int[] a) { for (int i = 1; i < a.length; i++) { for (int j = i; j > 0; j--) { if (a[j] < a[j - 1]) { int temp = a[j - 1]; a[j - 1] = a[j]; a[j] = temp; } else break; } }}
  • 快速排序

快速排序的基本思路如下:

  • 假设我们对数组{7, 1, 3, 5, 13, 9, 3, 6, 11}进行快速排序。
  • 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
  • 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。
  • 此时得到类似于这种排序的数组{3, 1, 3, 5, 6, 7, 9, 13, 11}。
  • 在初始状态下7是第一个位置,现在需要把7挪到中间的某个位置k,也即k位置是两边数的分界点。
  • 那如何做到把小于和大于基准数7的值分别放置于两边呢,我们采用双指针法,从数组的两端分别进行比对。
  • 先从最右位置往左开始找直到找到一个小于基准数的值,记录下该值的位置(记作 i)。
  • 再从最左位置往右找直到找到一个大于基准数的值,记录下该值的位置(记作 j)。
  • 如果位置i
  • 如果执行到i==j,表示本次比对已经结束,将最后i的位置的值与基准数做交换,此时基准数就找到了临界点的位置k,位置k两边的数组都比当前位置k上的基准值或都更小或都更大。
  • 上一次的基准值7已经把数组分为了两半,基准值7算是已归位(找到排序后的位置)。
  • 通过相同的排序思想,分别对7两边的数组进行快速排序,左边对[left, k-1]子数组排序,右边则是[k+1, right]子数组排序。
  • 利用递归算法,对分治后的子数组进行排序。

快速排序的优势

快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小于基准值的都交换到左边,大于基准值的都交换到右边,

这样不会像冒泡一样每次都只交换相邻的两个数,因此比较和交换的此数都变少了,速度自然更高。当然,也有可能出现最坏的情况,就是仍可能相邻的两个数进行交换。

快速排序基于分治思想,它的时间平均复杂度很容易计算得到为O(nlogn)。

实现代码如下:

public static void quickSort(int[] array) { int len; if (array == null || (len = array.length) == 0 || len == 1) { return; } sort(array, 0, len - 1);}// 递归实现快速排序public static void sort(int[] array, int left, int right) { if (left > right) { return; } // base中存放基准数 int base = array[left]; int i = left, j = right; while (i != j) { // 顺序很重要,先从右边开始往左找,直到找到比base值小的数 while (array[j] >= base && i < j) { j--; } // 再从左往右边找,直到找到比base值大的数 while (array[i] <= base && i < j) { i++; } // 上面的循环结束表示找到了位置或者(i>=j)了,交换两个数在数组中的位置 if (i < j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } // 将基准数放到中间的位置(基准数归位) array[left] = array[i]; array[i] = base; // 递归,继续向基准的左右两边执行和上面同样的操作 // i的索引处为上面已确定好的基准值的位置,无需再处理 sort(array, left, i - 1); sort(array, i + 1, right);}
  • JDK自带排序

Arrays.sort(arr);

在JDK1.7之前,JDK中自带的排序算法是经典快排,但是在JDK1.7的时候,JDK中自带的数组排序算法已经换成了Dual-Pivot Quicksort(双轴快速排序算法),该算法的时间复杂度是O(nLogn)。

JDK1.8中的排序算法如下:

/** * 归并排序中的最大运行次数 */private static final int MAX_RUN_COUNT = 67;/** * 归并排序中运行的最大长度 */private static final int MAX_RUN_LENGTH = 33;/** * 如果要排序的数组长度小于此常量,则使用快速排序优先于合并排序。 */private static final int QUICKSORT_THRESHOLD = 286;static void sort(int[] a, int left, int right, int[] work, int workBase, int workLen) { // Use Quicksort on small arrays if (right - left < QUICKSORT_THRESHOLD) { sort(a, left, right, true); return; } /* * Index run[i] is the start of i-th run (ascending or descending * sequence). */ int[] run = new int[MAX_RUN_COUNT + 1]; int count = 0; run[0] = left; // Check if the array is nearly sorted for (int k = left; k < right; run[count] = k) { if (a[k] < a[k + 1]) { // ascending while (++k <= right && a[k - 1] <= a[k]) ; } else if (a[k] > a[k + 1]) { // descending while (++k <= right && a[k - 1] >= a[k]) ; for (int lo = run[count] - 1, hi = k; ++lo < --hi;) { int t = a[lo]; a[lo] = a[hi]; a[hi] = t; } } else { // equal for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k];) { if (--m == 0) { sort(a, left, right, true); return; } } } /* * The array is not highly structured, use Quicksort instead of * merge sort. */ if (++count == MAX_RUN_COUNT) { sort(a, left, right, true); return; } } // Check special cases // Implementation note: variable "right" is increased by 1. if (run[count] == right++) { // The last run contains one element run[++count] = right; } else if (count == 1) { // The array is already sorted return; } // Determine alternation base for merge byte odd = 0; for (int n = 1; (n <<= 1) < count; odd ^= 1) ; // Use or create temporary array b for merging int[] b; // temp array; alternates with a int ao, bo; // array offsets from 'left' int blen = right - left; // space needed for b if (work == null || workLen < blen || workBase + blen > work.length) { work = new int[blen]; workBase = 0; } if (odd == 0) { System.arraycopy(a, left, work, workBase, blen); b = a; bo = 0; a = work; ao = workBase - left; } else { b = work; ao = 0; bo = workBase - left; } // Merging for (int last; count > 1; count = last) { for (int k = (last = 0) + 2; k <= count; k += 2) { int hi = run[k], mi = run[k - 1]; for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) { if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) { b[i + bo] = a[p++ + ao]; } else { b[i + bo] = a[q++ + ao]; } } run[++last] = hi; } if ((count & 1) != 0) { for (int i = right, lo = run[count - 1]; --i >= lo; b[i + bo] = a[i + ao]) ; run[++last] = right; } int[] t = a; a = b; b = t; int o = ao; ao = bo; bo = o; }}

有关Dual-Pivot Quicksort(双轴快速排序算法)的讲解可参考如下几篇文章:

  • https://blog.csdn.net/Holmofy/article/details/71168530
  • https://www.jianshu.com/p/6d26d525bb96
  • https://rerun.me/2013/06/13/quicksorting-3-way-and-dual-pivot/
  • https://www.jianshu.com/p/2c6f79e8ce6e

数组反转

public static void fanzhuan(int[] a) { for (int i = 0; i < a.length / 2; i++) { int tp = a[i]; a[i] = a[a.length - i - 1]; a[a.length - i - 1] = tp; }}

也可以将数组转为ArrayList,然后调用Collections.reverse(arrayList);进行反转

查找

最笨的方法,就是从前往后一个个的查找,这种方式不到不得以,不要使用,太笨。

  • 二分查找

二分查找的实现思路:

1. 定义查找的范围,也就是开始索引(如 int start = 0)和结束索引(如 int end = srr.length - 1)。

2. 判断 start 是否小于等于 end ,如果 start 大于 end,则结束查找,直接返回-1代表没有找到所查找的元素。如果满足条件,则计算出 start 和 end 之间的中间索引 middle ,并获取该中间索引对应的值 middleVal。

  • int middle = (start + end)/2.
  • int middleVal = arr(middle);

3. 把中间索引对应的值 middleVal 和要查找的元素 key 进行比较:

  • 如果 middleVal 等于 key,就返回当前的中间索引 middle;
  • 如果 middleVal 大于 key:
  • 对于升序数组:end = middle - 1;
  • 对于降序数组:start = middle + 1;
  • 如果 middleVal 小于 key:
  • 对于升序数组:start = middle + 1;
  • 对于降序数组:end = middle - 1;

4. 重新执行第二步操作。

使用二分查找前,必须对数据进行排序,如果未排序,则有可能找不到所查找的元素。如果数组包含多个指定值的元素,则不确定返回哪个位置上的该元素。

public static int binarySearch(int[] arr, int key) { // 在不断缩小范围的过程中,可以 // 返回-1则说明找不到这个数 // 定义起始、终点、中间索引,目标key值索引 int start = 0; int end = arr.length - 1; // 在数组中找要找的数,因为不一定会一下子找到,所以这应该是一个重复寻找的过程,即会用到循环 while (start <= end) {// 看出start不断增大,end不断缩小;如果当start和end相等时都还找不到,start会继续增加,end继续变小,此时这已经不是一个正常的数组,结束循环 int middle = (start + end) / 2; int value = arr[middle]; // 让中间索引对应的值value与要查找的值key进行比较 if (key == value) { // 如果相等,即找到,则返回中间索引,并跳出循环 return middle; } else if (key > value) { // key > value if (arr[0] < arr[1]) { // 升序 start = middle + 1; } else { // 降序 end = middle - 1; } } else { // key < value if (arr[0] < arr[1]) { // 升序:end = middle - 1 end = middle - 1; } else {// 降序:start = middle + 1 start = middle + 1; } } } // while括号 return -1;}
  • jdk自带的二分查找

Arrays.binarySearch(arr, val);

public static int binarySearch(int[] a, int key) { return binarySearch0(a, 0, a.length, key);}......private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) { int low = fromIndex; int high = toIndex - 1; while (low <= high) { int mid = (low + high) >>> 1; int midVal = a[mid]; if (midVal < key) low = mid + 1; else if (midVal > key) high = mid - 1; else return mid; // key found } return -(low + 1); // key not found.}

数组操作工具类

public class GenericArray { private T[] data; private int size; // 根据传入容量,构造Array public GenericArray(int capacity) { data = (T[]) new Object[capacity]; size = 0; } // 无参构造方法,默认数组容量为10 public GenericArray() { this(10); } // 获取数组容量 public int getCapacity() { return data.length; } // 获取当前元素个数 public int count() { return size; } // 判断数组是否为空 public boolean isEmpty() { return size == 0; } // 修改 index 位置的元素 public void set(int index, T e) { checkIndex(index); data[index] = e; } // 获取对应 index 位置的元素 public T get(int index) { checkIndex(index); return data[index]; } // 查看数组是否包含元素e public boolean contains(T e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; } // 获取对应元素的下标, 未找到,返回 -1 public int find(T e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; } // 在 index 位置,插入元素e, 时间复杂度 O(m+n) public void add(int index, T e) { checkIndex(index); // 如果当前元素个数等于数组容量,则将数组扩容为原来的2倍 if (size == data.length) { resize(2 * data.length); } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++; } // 向数组头插入元素 public void addFirst(T e) { add(0, e); } // 向数组尾插入元素 public void addLast(T e) { add(size, e); } // 删除 index 位置的元素,并返回 public T remove(int index) { checkIndexForRemove(index); T ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; data[size] = null; // 缩容 if (size == data.length / 4 && data.length / 2 != 0) { resize(data.length / 2); } return ret; } // 删除第一个元素 public T removeFirst() { return remove(0); } // 删除末尾元素 public T removeLast() { return remove(size - 1); } // 从数组中删除指定元素 public void removeElement(T e) { int index = find(e); if (index != -1) { remove(index); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("Array size = %d, capacity = %d 

arrays中copyof复制两个数组_数据结构与算法(3)数组相关推荐

  1. arrays中copyof复制两个数组_数组,及二维数组

    1.1 命令行参数(C) 在程序运行过程中,可以向应用程序传递一些参数,这些参数称为命名行参数. public 命令行参数以字符串的形式传入args数组中.可以一次传递0-多个参数,以空格分割. 如果 ...

  2. arrays中copyof复制两个数组_Java教程分享之数组知识梳理

    Java是一门面向对象编程语言,具有简单易用.功能强大的特征.数组是同类型数据的有序集合,在Java中是引用数据类型,引用数据类型值都存储在堆中.有很多新手初学Java数组觉得难度大,接下来就给大家简 ...

  3. arrays中copyof复制两个数组_Core Java - Arrays

    int 要找出一个array有多少个elements,使用array.length. array一旦被创建之后,无法更改它的大小(size).如果需要动态增加一个数组的大小,可以使用array lis ...

  4. arrays中copyof复制两个数组_Java的数组初识和拷贝用法

    方法重载:方法名称相同,参数列表不同. 不能有两个名字相同.参数类型相同,返回值不同的方法. 在进行方法重载时,方法的返回值一定相同!!! 方法递归特点: 1.必须有结束条件 2.每次递归处理时,一定 ...

  5. arrays中copyof复制两个数组_C语言100题集合026-使用指针交换两个数组中的最大值

    系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...

  6. arrays中copyof复制两个数组_异或的魅力!图解「数组中两个数的最大异或值」

    今天分享的题目来源于 LeetCode 第 421 号问题:数组中两个数的最大异或值.在 异或 这个知识点里面属于一个中高难度的题目. 题目描述 给定一个非空数组,数组中元素为 a0, a1, a2, ...

  7. arrays中copyof复制两个数组_Python数组切片中的复制与否问题-list篇

    说到Python中数组的切片操作,稍有了解的想必都不陌生.以Python的内置数据类型list(列表)为例, L = [5, 2, 0, 1, 3, 1, 4] L1 = L[3:7] 我们称L[3: ...

  8. @value 数组_数据结构与算法:12 数组与稀疏矩阵

    12 数组与稀疏矩阵 知识结构: 图1 知识结构 1. 数组 1.1 数组的定义 数组是具有一定顺序关系的若干对象组成的集合,组成数组的对象称为数组元素. 例如: 向量对应一维数组 矩阵对应二维数组 ...

  9. python中判断无向图是否有环_数据结构与算法:17 图

    17 图 知识结构: 图1 知识结构 1. 图的基本概念与术语 1.1 图的定义 图由顶点集和边集组成,记为 . 顶点集:顶点的有穷非空集合,记为. 边集:顶点偶对的有穷集合,记为 . 边: 无向边: ...

最新文章

  1. 区别 eks_sport 和 exercise 有什么区别?看完你就清楚了!
  2. reddit_如何使用Python创建自定义Reddit通知系统
  3. 监听文件修改,自动加载xml文件。
  4. linux 网卡丢弃多播包,rp_filter及Linux下多网卡接收多播的问题
  5. 今天痛下决心,把开发人员的外网给断了,不断是不好管了,人心散了队伍就不好带...
  6. 如何应对数据库CPU打满?最优解在这里...
  7. 最全三大框架整合(使用映射)——数据库资源文件jdbc.properties
  8. Taro+react开发(7)--控制跳转
  9. 毛概 第二章新民主主义革命理论
  10. 中音萨克斯指法表图_初学萨克斯一定要了解这6点基础知识
  11. 【免费下载】2021年4月热门报告盘点下载
  12. centos中mysql启动失败,解决CentOS下mysql启动失败
  13. C语言实现文件复制 fgetc、fputc函数的使用 带详细注释版
  14. 制作一个小木马的步骤
  15. CuteFTP试用期后继续免费使用
  16. MacBook入门之——添加打印机
  17. Spring使用标签aop:aspectj-autoproxy 出的一些错
  18. MATLAB--数字图像处理 特征点匹配
  19. 以太坊Ropsten测试网合并意味着什么?
  20. Neo4j导入本地CSV文件三元组关系生成图谱

热门文章

  1. web前端入门学习(纯干货)
  2. 在vue中使用SockJS实现webSocket通信
  3. DIV CSS浏览器的兼容性
  4. 【HTML基础】表格和表单
  5. 获取DOM元素方法小结
  6. srtvlet filter
  7. [POJ1463] Strategic game
  8. Java组合实体模式~
  9. Linux 之目录 -鸟哥的Linux私房菜
  10. javascript 事件委派