你所不知道的数组

120 阅读6分钟

为什么数组下标从0开始?

多维数组

不同语言的数组

查找算法

排序算法

数组的特性

我们前面说过,数据结构包括线性结构、树、堆、图,数组呢就是一种线性结构。

它看起来长这样

1483F828-0AF6-4093-BCAA-E51C671535A9.png

它有两个约束

  1. 连续的内存空间
  2. 每个元素类型相同

有这两个特性,我们可以很方便的通过下标访问到任意一个元素,也即根据数组的首地址+偏移量算出任意元素的地址,从而获取到它的值。

a[i].地址=基地址+i*byte_size_of_item

因为这两个特性,所以数组的下标设计成从0开始,以至于可以更高效方便的计算任意元素的地址

也因为数组的这种特性,在对数组进行插入、删除的时候需要付出比较多的代价。

数组的查找

数组的查找算法主要包含两类 无序查找和有序查找,主要区别是对于数据组织方式的要求

顺序查找

也称线性查找,对数据组织形式无要求,从开始一直扫描到结尾,逐个处理的方式

//顺序查找 int SequenceSearch(int a[], int value, int n) { int i; for(i=0; i<n; i++) if(a[i] == value) return i; return -1; }

二分查找

也称折半查找,要求数据按顺序组织

//二分查找,非递归版本 public static int binarySearch2(int[] array, int key) { int low = 0; int high = array.length - 1; while (low <= high) { //防止数组越界,且位运算比除法运算快 int mid = low + ((high - low) >> 1); if (key == array[mid]) { return mid; } if (key > array[mid]) { low = mid + 1; } if (key < array[mid]) { high = mid - 1; } } return -1; }

可见二分查找的核心就是定义两个游标,按逻辑不断缩小两个游标的。

插值查找

要求数据有序均匀分布

//插值查找 int InsertionSearch(int a[], int value, int low, int high) { if(low <= high) { int mid = low+(value-a[low])/(a[high]-a[low])*(high-low); // 关键 if(a[mid]==value) return mid; if(a[mid]>value) return InsertionSearch(a, value, low, mid-1); if(a[mid]<value) return InsertionSearch(a, value, mid+1, high); } else return -1; }

对比二分查找,可以看出核心在于mid的求职方式改变,也即系数1/2变成了value-a[low]/a[high]-a[low],这个公式表达的意思就是调整到更靠近value的位置。

斐波那契查找

也是一种二分查找的改进

数组的排序

从这些常见的算法,可以看出一般都要求数据按序组织,那常见的排序算法有哪些?

冒泡排序

它是从左到右依次两两比较,把大数不断右移的过程

  • 稳定性:稳定
  • 时间复杂度:最佳:�(�) ,最差:�(�2), 平均:�(�2)
  • 空间复杂度:�(1)
  • 排序方式:In-place
  • 应用场景:小数据

public static void bubbleSort(int[] arr) { int temp = 0; for (int i = arr.length - 1; i > 0; i--) { // 每次需要排序的长度 for (int j = 0; j < i; j++) { // 从第一个元素到第i个元素 if (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } }//loop j }//loop i }// method bubbleSort

使用数组可以满足基本的增删查改,

选择排序

类似于冒泡,它是从左到右,固定i,遍历i之后的所有元素,符合条件的与i交换。

  • 稳定性:不稳定

  • 时间复杂度:最佳:O(n^2),最差:O(n^2), 平均:O(n^2)

  • 空间复杂度:O(1)

  • 排序方式:In-place

public static void selectionSort(int[] arr) { int temp, min = 0; for (int i = 0; i < arr.length - 1; i++) { min = i; // 循环查找最小值 for (int j = i + 1; j < arr.length; j++) { if (arr[min] > arr[j]) { min = j; } } if (min != i) { temp = arr[i]; arr[i] = arr[min]; arr[min] = temp; } } }

插入排序

原理是不断构建有序序列,遍历未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

  • 稳定性:稳定
  • 时间复杂度:最佳:O(n) ,最差:O(n^2), 平均:O(n^2)
  • 空间复杂度:O(1)
  • 排序方式:In-place

public static void insertionSort(int[] arr){ for (int i=1; i<arr.length; ++i){ int value = arr[i]; int position=i; while (position>0 && arr[position-1]>value){ arr[position] = arr[position-1]; position--; } arr[position] = value; }//loop i }

归并排序

分治的典型应用,它把数组分割成两个较小的子数组来解决问题,然后递归地对这两个子数组进行排序,最后将排序好的子数组合并以产生最终的排序数组。

过程:

  1. 分割:将数组从中间分割成两个子数组,不断递归地分割子数组,直到每个子数组只有一个元素。
  2. 排序:递归地对两个子数组进行排序。一个单元素数组自然是已排序的,所以这个步骤主要适用于多于一个元素的数组。
  3. 合并:将两个已排序的子数组合并成一个有序的数组。合并操作是归并排序的核心,需要比较来自两个子数组的元素,以决定它们的排列顺序。

时间复杂度:O(nlogn)稳定的排序算法

优点

  • 性能稳定,对于任何数据集都是 O(n log n) 时间复杂度。
  • 非常适合于大数据集,特别是在数据不能一次性装入内存时。
  • 是一种稳定的排序算法。

缺点

  • 需要额外的内存空间来合并两个子数组。
  • 在小数据集上可能不如其他简单排序(如插入排序)高效。

应用场景

大型数据集的排序,尤其是在数据量太大,无法一次性装入内存时。它常用于外部排序和并行排序算法中。此外,归并排序的稳定性也使其在需要保持相同元素之间原有顺序的应用中非常有用。

void merge(int arr[], int l, int m, int r) { int i, j, k; int n1 = m - l + 1; int n2 = r - m; // 创建临时数组 int L[n1], R[n2]; // 复制数据到临时数组L[] 和 R[] for (i = 0; i < n1; i++) L[i] = arr[l + i]; for (j = 0; j < n2; j++) R[j] = arr[m + 1 + j]; // 合并临时数组回arr[l..r] i = 0; // 初始索引第一个子数组 j = 0; // 初始索引第二个子数组 k = l; // 初始索引合并的子数组 while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } // 复制L[]的剩余元素 while (i < n1) { arr[k] = L[i]; i++; k++; } // 复制R[]的剩余元素 while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(int arr[], int l, int r) { if (l < r) { // 找到中间索引 int m = l + (r - l) / 2; // 分别对左右半部分排序 mergeSort(arr, l, m); mergeSort(arr, m + 1, r); // 合并已排序子集 merge(arr, l, m, r); } }

此外还有一些使用其它辅助结构的如堆排序、桶排序、计数排序、基数排序等。