冒泡排序
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = arr.length - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//时间复杂度 o(N**2)
//空间复杂度 o(1)
选择排序
0-N之间找一个最小的数放在0位置上
然后1-N之间再找相应的数放在1位置上 以此类推
min记录的数组下标 交换的也是数组下标
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
min = arr[min] < arr[j] ? min : j;
}
swap(arr, i, min);
}
}
//时间复杂度 o(N**2)
//空间复杂度 o(1)
插入排序
类似于打牌的时候码牌
0位置一开始是不动的
0-1位置上比较 0<1就行交换
0-2 再看2位置上 和1比较 大的话不动 小的话继续和0比较 看大小
0-3位置上以此类推 找到合适的位置插进去
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
//时间复杂度 o(N**2)
//空间复杂度 o(1)
归并排序
分治的思想
mergeProcess将整个数组不停的划分成子问题 然后还原现场实现排序
左右都排好了 弄一个辅助空间help
然后设置了两个指针分别指向左右的第一个元素,然后往help里面放
左排完然后剩下了右全部塞到help中 最后在进行替换
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeProcess(arr, 0, arr.length - 1);
}
public static void mergeProcess(int[] arr, int L, int R) {
if (L == R) {
return;
}
int mid = (L + R)/2;
mergeProcess(arr, L, mid); //左部分
mergeProcess(arr, mid + 1, R); //右部分
merge(arr, L, mid, R); //整合左右 让其整体有序
}
public static void merge(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L; //指向左侧部分第一个数
int p2 = mid + 1; //指向右侧部分第一个数
while (p1 <= mid && p2 <= R) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= R) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < arr.length; i++) {
arr[i] = help[i];
}
}
//时间复杂度 o(N*logN)
//空间复杂度 o(N)
经典快排
整个数组 把数组的最后一个元素x作为划分值 小于等于x的放在左边 大于x的放在右边
左右部分分成两块 然后分别再选最后一个数作为划分 在划在分等等
因为有了荷兰国旗问题 可以让等于区域不动 只去管大于区域和小于区域
存在的问题:按照最后一个元素作为划分不能使大于区域和小于区域规模大致相同
例如[1, 2, 3, 4, 5, 6]这样的话就没有大区域了
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int L, int R) {
if (L < R) {
int[] p = partition(arr, L, R); //返回的等于区域
quickSort(arr, L, p[0] - 1); //小于区域继续进行划分
quickSort(arr, p[1] + 1, R); //大于区域继续进行划分
}
}
public static int[] partition(int[] arr, int L, int R) {
int less = L - 1;
int more = R; //参照荷兰国旗问题 R + 1
while (L < more) {
if (arr[L] < arr[R]) {
swap(arr, ++less, L++);
} else if (arr[L] > arr[R]) {
swap(arr, --more, L);
} else {
L++;
}
}
swap(arr, more, R); //因为最后一个元素是参考条件没有参加遍历
return new int[] {less + 1, more}; // 返回等于区域的下标[p[0], p[1]]
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
随机快排
改进经典快排
不把最后一个数进行划分 而是拿一个随机的数和最后一个数交换 作为参考
这个时候复杂度就变成了概率事件,变成了长期期望问题
//swap(arr, L + (int)(Math.random() * (R - L + 1)), R);
//空间复杂度 o(logN) 怎么来的?
等于区域是要记下来的 这样我们才能知道下面的子过程在哪里开始 空间浪费在了划分点上 这个断点就相当于一个二叉树的高度
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int L, int R) {
if (L < R) {
swap(arr, L + (int)(Math.random() * (R - L + 1)), R);
int[] p = partition(arr, L, R); //返回的等于区域
quickSort(arr, L, p[0] - 1); //小于区域继续进行划分
quickSort(arr, p[1] + 1, R); //大于区域继续进行划分
}
}
public static int[] partition(int[] arr, int L, int R) {
int less = L - 1;
int more = R;
while (L < more) {
if (arr[L] < arr[R]) {
swap(arr, ++less, L++);
} else if (arr[L] > arr[R]) {
swap(arr, --more, L);
} else {
L++;
}
}
swap(arr, more, R);
return new int[] {less + 1, more}; // 返回等于区域的下标[p[0], p[1]]
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//时间复杂度 o(N*logN)
//空间复杂度 o(logN)
堆排序
i的左孩子2i+1 右孩子2i+2 父亲(i-1)/2
大根堆和小根堆 就是完全二叉树 实际上是数组结构 逻辑上是二叉树结构
大根堆 任何子数的最大值都是头部
小根堆 任何字数的最小值都是头部
怎么把完全二叉树变成大根堆
形成一个堆 数组中的数挨个插入向上一次比对
只要我比父亲大我就来到我的父位置 然后再比较当前父亲 继续继续
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index + 1) / 2]) {
swap(arr, index, (index + 1) / 2);
index = (index + 1) / 2;
}
}
//时间复杂度o(log(N))
假设数组中有一个值突然变小了 怎么改变还能使他形成大根堆
孩子中哪个大哪个就上去
public static void heapify(int[] arr, int index, int heapSize) {
//heapSiza 完全二叉树的大小 并不一定是数组的大小
int left = index * 2 + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ?
left + 1 : left;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
堆排序 利用堆结构去排序
1.变成大根堆
2.整个数组变成堆结构后,让堆顶和最后一个元素交换位置,然后让堆大小-1之后 在进行heapify,heapSize下在形成堆结构继续这样的操作
3.每次都把堆最大的位置放到数组最后面 这样完成排序
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i); //0 - i 之间形成大根堆
}
int heapSize = arr.length;
swap(arr, 0, --heapSize); //最后一个屋位置上的数和0位置交换
while (heapSize > 0) {
heapify(arr, 0, heapSize); //0到N-1位置上继续heapify
swap(arr, 0, --heapSize); //继续交换
}
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index + 1) / 2]) {
swap(arr, index, (index + 1) / 2);
index = (index + 1) / 2;
}
}
public static void heapify(int[] arr, int index, int heapSize) {
//heapSiza 完全二叉树的大小 并不一定是数组的大小
int left = index * 2 + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ?
left + 1 : left;
if (largest == index) {
break;
}
swap(arr, largest, index); //largest != index
index = largest;
left = index * 2 + 1;
}
}
排序的稳定性
稳定性 指的是 相同值得相对位置相对不变
[1, 2, 3, 3, 3, 5, 2, 3]
排序后[1, 2, 2, 3, 3, 3, 3, 5]第一个三第二个三等相对次序不会发生改变 那么具有稳定性
冒泡排序:大的值一直向后交换 碰到相等的值 只要保证二者不进行交换 那么就能保证其稳定性
插入排序:还是碰到相同的不进行交换就能保证其稳定性
选择排序: [1, 1, 2, 3, 3, 3, 0] => [0, 1, 2, 3, 3, 3, 1]做不到稳定性
归并排序:保证在merge的过程中先copy左区域就可以保证稳定性
快速排序:partation过程中做不到稳定性
堆排序:每次交换的过程中不会去关心相等值做不到稳定性
荷兰国旗问题 - 快排的思想去理解
一个数组中划分出小于区域,等于区域和大于区域。
public static int[] partition(int[] arr, int L, int R, int num) {
int less = L - 1;
int more = R + 1;
int cur = L;
while (cur < more) {
if (arr[cur] < num) {
swap(arr, ++less, cur++);
} else if (arr[cur] > num) {
swap(arr, --more, cur);
} else {
cur++;
}
}
return new int[] {less + 1, more - 1}; //返回等于部分的下标
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
[1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 4, 5, 7, 8, 9]
num = 5
小于区域在左边 等于区域在中间 大于区域在右边
cur = 0
然后和num比较大小 小的话和小与区域最左边的数交换 等于的话直接跳下一个位置 大于的话和大于区域的前一个位置进行交换
整个数组在整个过程中其实被划分成了四个部分[小于部分, 等于部分, 待定部分,大于部分]
放到小于部分的时候 是需要cur++ 的 而放到大于部分则不需要 因为小于部分和等于部分是连在一起的 交换的过程中小于部分推着等于部分走 而大于部分则是和待定区域的元素进行交换 把大的换走了 又换来一个新的 这个时候就要看看这个新的和num的大小关系
小和问题 - 可用归并排序去思考
在一个数组中每一个数左边比当前数小的数累加起来叫做这个数组的小和。
o(N2) 各个部分都遍历一遍 然后累加
可用归并排序去思考
还是左右部分
右边部分大于p1 那么整个后面的部分都会大于p1 这个数量就是R-p2+1
用p1这个数乘以这个数量 那么就是p1这个数在整个过程中被cao的次数。。
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) / 2;
return mergeSort(arr, L, mid) + mergeSort(arr, mid + 1, R) + merge(arr, L, mid, R);
}
public static int merge(int[] arr, int L, int mid, int R) {
int[] help = new int[R - L + 1];
int i = 0;
int p1 = 1;
int p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= R) {
res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < arr.length; i++) {
arr[i] = help[i++];
}
return res;
}