java数据结构与算法2---数组及其算法(1)
数组介绍
提起数组想必各位小可爱们并不陌生,在我们学习任何一门语言时最常接触到的就是数组了,并且数组也是数据结构与算法的开门石,充分理解数组的结构与特性是相当重要的,在java数据结构与算法系列博客中,我将以数组为起点与大家伙们共同学习整个数据结构与算法。
数组结构(如下图):数组在内存中是一段连续的空间,一经创建数组对象的大小就会固定下来不能改变。由于数组必须是连续的,当我们在进行插入删除操作时常常会面临插入或删除点之后的数据都需要后移一位或前移一位,这样就使数组的插入删除的效率较低,同时这也是数组的致命缺点之一。
删除数据项如下图:
无序数组:无序数组是指数组中存取的数据是乱序的没有规则的。这种数组在进行插入的时候直接在数组末尾操作是高效的;但是在进行查找指定数据的位置的时候必须从头开始遍历是低效的。
有序数组:有序数组是指数组中存取的数据是有序到的按照某种规律依次排列的。这种数组在进行查找指定数据的时候可以采用更加高效的二分查找效率较高;但是在进行插入的时候要先确定插入点然后将插入点以后的数据都后移一位最后插入数据。
数组常用排序算法
冒泡排序
冒泡排序(如下图):依次遍历数组当左边的数比右边的数大就交换,使得每次遍历的最大数都依次排在数组的尾部,形成一种气泡(大数)向水面上(数组尾部)冒出的效果。因其效率低下,冒泡排序在实际开发中基本不会使用。
代码如下:( 时间复杂度:O(N^2) 空间复杂度:O(1) )
//冒泡排序方法
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
选择排序
选择排序:通过遍历数组得到数组最小值的下标索引,利用记录下的最小值下标索引与数组最前端位置交换,就这样第一次遍历整个数组0—n-1将a[min]与a[0]交换,第二次遍历数组1—n-1 将a[min]与a[1]交换,直至整个数组遍历结束。因其效率低下,选择排序在实际开发中基本不会使用。
代码如下:( 时间复杂度:O(N^2) 空间复杂度:O(1) )
//选择排序方法
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
插入排序
插入排序:简单的理解为将一个数据向有序数组中插入,这里要插入的数据排在有序数组末尾,通过判断a[i]>a[i+1]?互换i-- :不动i--;插入排序的效率还好但并不是实际开发中的主流排序算法,开发过程中可能会使用到。
代码如下:( 时间复杂度:O(N^2) 空间复杂度:O(1) )
//插入排序方法
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
外排
外排: 针对两个有序数组才能实现,利用两个指针分别指向两个数组开头位置,然后开辟额外空间arr,利用两个指针所指的数据比较大小,小者放入arr中其指针后移一位,当出现指针越界后把未越界的指针剩余未插入arr的所有数据拷贝至arr中即可完成排序。
代码如下 :( 时间复杂度:O(N) 空间复杂度:O(N) )
//外排算法
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
归并排序
*归并排序(对外排的加深):利用递归来实现的算法,我们可以理解为先将数组arr等分为两份且每份的数组都是有序的,然后开辟等数组的空间,向这个新开辟的空间中依次从小到大插入数据,最后将新开辟的空间数据拷贝到arr并销毁新开辟的空间;依次从小到大插入数据的实现可以理解为有两个指针分别指向两个数组的开头,然后判断指针对应的数据,小者插入空间,并且将小者对应的指针+1,直至有一个数组的指针越界就将还没有插入进去的数组数据全都依次拷贝到新开辟的空间中即可。归并排序在实际开发中使用较多。
代码如下:( 时间复杂度:O(N*logN) 空间复杂度:O(N) )
//归并排序
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1); //防止下标越界
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
//外排算法
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
知识拓展:小和问题和逆序对问题
提示:小和问题和逆序对问题都是采用归并排序来求解
小和问题:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。
例子:求数组 [1,3,4,2,5]的小和为?(过程:1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16)
对于小和问题首先进行逻辑转换:小和=x*x后面比x大的数的个数,(x对应数组0~n-1的每一个取值)。例如:求数组[1,3,4,2,5]的小和就是求1*4+3*2+4*1+2*1+5*0=16
代码实现如下:
//小和问题算法实现
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
//归并排序
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid) + mergeSort(arr, mid + 1, r) + merge(arr, l, mid, r);
}
//外排算法
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0; //利用res实现小和求解
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
显然逆序对问题跟小和问题大同小异,也就是在上面的merge方法中,实现当arr[p1]>arr[p2]时打印arr[p1]和arr[p2]即可。
我将在java数据结构与算法2---数组及其算法(2)中介绍数组常用排序算法——快排(传统快排和随机快排)、堆排序
敬请关注! 点赞+关注不迷路哟!
谢谢阅读 ---by 知飞翀