一、快速排序
public class QuickSort {
public static void main(String[] args) {
int[] arr = {6, 3, 5, 4, 9, 1, 7, 8, 2};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
/**
* 使数组在 l到 r上有序
*
* @param arr
* @param l
* @param r
*/
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
// 如果加上下面这一行就是随机快排,从当前数组中随机选一个数和最后一个数交换,然后用这个数来作划分
//swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
// p中固定只有两个元素,第一个表示等于区域开始位置,第二个表示等于区域结束位置
int[] p = partition(arr, l, r);
// 对小于区域进行排序
quickSort(arr, l, p[0] - 1);
// 对大于区域进行排序
quickSort(arr, p[1] + 1, r);
}
}
/**
* 一次 partition过程,返回等于区域的起始下标
*
* @param arr
* @param l 数组开始的下标
* @param r 数组结束的下标
* @return
*/
public static int[] partition(int[] arr, int l, int r) {
// 小于区域边界,最开始从 l-1开始
int less = l - 1;
// 大于区域边界,因为我们选取的就是最后一个元素,作为比较,所以不参与,从 r开始
int more = r;
// 用 l作为cur,当cur小于大于区域的边界
while (l < more) {
// 以最后一个元素为基准进行划分
if (arr[l] < arr[r]) {
// 小于区域的前一个数和当前数交换,比较下一个数
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
// 大于区域的前一个数和当前数交换,继续比较当前数
swap(arr, --more, l);
} else {
// 相等跳过
l++;
}
}
// 最后一个数没有参与排序,完成上面的比较后将最后一个数放到比较后的数组中,位置就是more的位置
swap(arr, more, r);
// 返回等于区域闭区间下标
// less为小于区域最后一个数的下标,more为等于区域最后一个数的下标
return new int[]{less + 1, more};
}
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,7]时,经典快排选取最后一个数作为划分来比较其他的数,partition一遍的时候只搞定7这个数, 然后对剩下的[1,2,3,4,5,6]选择6进行partition又只搞定一个数(对比冒泡排序),对于n个这样的数,每一次partition都是O(n)的复杂度,所以这时整个复杂度就成了O(n^2)了。解决方法就是随机选取一个数和最后一个数交换,然后用最后一个数来进行划分,使得划分后大于区域和小于区域的数据量差不多,无论最好还是最坏情况它都是一个概率事件,在大样本环境下,经过概率学证明后可以得到随机快排时间复杂度可以达到O(N*logN)。
空间复杂度用于记录递归时每次划分位置的数,在最好情况下每次划分都在中间,空间复杂度O(logn),比如 [1,2,3,4,5,6,7,8],我们需要使用partition记录断点位置,断点如果都打在中间,则8条数据只需要记录3次断点即可,这为最好情况,但如果断点打在8,第二次打在7,第三次打在6…则8条数据,一共需要记录8次断点,这就为最差情况为O(n)。
- 关于随机值的获取:取
[x, y]
区间整数:x + (int) (Math.random() * (y - x + 1))
二、归并排序
public class MergeSort {
public static void main(String[] args) {
int[] arr = {9, 3, 8, 1};
sortProcess(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
/**
* 排序过程
*
* @param arr
* @param l 起始下标
* @param r 结束下标
*/
public static void sortProcess(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
// 左边归并排序
sortProcess(arr, l, mid);
// 右边归并排序
sortProcess(arr, mid + 1, r);
merge(arr, l, mid, r);
}
/**
* 合并两个有序数组
*
* @param arr
* @param l 起始小标
* @param mid 中间位置(左边数组结束下标)
* @param r 结束下标
*/
public static void merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
// 指向左边数组起始位置
int pLeft = l;
// 指向右边数组起始位置
int pRight = mid + 1;
// 都没有到头,取出来比较
while (pLeft <= mid && pRight <= r) {
// 谁小谁填到 help数组中
help[i++] = arr[pLeft] < arr[pRight] ? arr[pLeft++] : arr[pRight++];
}
// 右边的填完了 pRight超过 r
while (pLeft <= mid) {
help[i++] = arr[pLeft++];
}
// 左边的填完了 pLeft超过 mid
while (pRight <= r) {
help[i++] = arr[pRight++];
}
// 拷贝回原数组
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
}
- 时间复杂度:O(nlogn)
- 空间复杂度:O(N),归并排序需要一个与原数组相同长度的数组做辅助来排序
- 稳定性:归并排序是稳定的排序算法,help[i++] = arr[pLeft] < arr[pRight] ? arr[pLeft++] : arr[pRight++];这行代码可以保证当左右两部分的值相等的时候,先复制左边的值,这样可以保证值相等的时候两个元素的相对位置不变。
2.1 求数组的小和
/**
* 小和问题
*
* [3, 1, 4, 6, 5, 2]
* 3左边比3小的数:0
* 1左边比1小的数:0
* 4左边比4小的数:3, 1
* 6左边比6小的数:3, 1, 4
* 5左边比5小的数:3, 1, 4
* 2左边比2小的数:1
* 所以小和为:1*4 + 3*3 + 4*2 = 21
*
* 等价于求右边有多少个数,比当前数大
* 3右边比3大的数有3个
* 1右边比1大的数有4个
* 4-------------2
* 5-------------0
* 2-------------0
*/
public class MergeSortSmallSum {
public static void main(String[] args) {
int[] arr = {3,1,4,6,5,2};
System.out.println(mergeSort(arr, 0, arr.length - 1));
}
/**
* 返回在l到r上产生的小和
* @param arr
* @param l
* @param r
* @return
* (l+r)/2 l+(r-l)/2
* a/2 = a >> 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 mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= r) {
// 如果 p1 < p2,那么右边的数都比p1大,共有(r - p2 + 1)个
res += arr[p1] < arr[p2] ? arr[p1] * (r - p2 + 1) : 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 < help.length; i++) {
arr[l+i] = help[i];
}
return res;
}
}
在一次merge的过程中,我们可以求出左边数组中的每一个数对于右边数组来说,有多少个数比当前数大,例如[1, 3, 4]
和[2, 5, 6]
在merge的过程中,可以求出有3个比1大的数,有2个比3大的数,有2个比4大的数
2.1 求数组的逆序对
/**
* @auther Mr.Liao
* @date 2019/11/6 16:46
* 在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
* [9, 3, 8, 1]
* 93 98 91 31 81
*/
public class MergeSort_nixudui {
static int count = 0;
public static void main(String[] args) {
int[] arr = {4,5,6,7};
sortProcess(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
System.out.println(count);
}
public static void sortProcess(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = (l + r) / 2;
// 左边归并排序
sortProcess(arr, l, mid);
// 右边归并排序
sortProcess(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) {
if (arr[p1] > arr[p2]) {
help[i++] = arr[p2++];
// 因为如果arr[p1]此时比右数组的当前元素arr[p2]大,
// 那么左数组中arr[p1]后面的元素就都比arr[p2]大
count += mid - p1 + 1;
} else {
help[i++] = arr[p1++];
}
}
// 右边的到 r位置了
while (p1 <= mid) {
help[i++] = arr[p1++];
}
// 左边的到 mid位置了了
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
}
三、堆排序
完全二叉树:完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐,堆就是完全二叉树,数组——完全二叉树(通过下标来维护彼此的关系)
大根堆:当前树(子树)中(包含子树)的最大值都是头部
小根堆:当前树(子树)中(包含子树)的最小值都是头部
数组——>大根堆
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = (int)(Math.random() * 100);
}
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 形成大根堆 复杂度:O(n)
for (int i = 0; i < arr.length; i++) {
// 0 ~ i 之间形成大根堆
heapInsert(arr, i);
}
// 大根堆中节点的个数
int heapSize = arr.length;
// 堆顶位置的数和最后一个数交换,最大值来到了数组的最后,堆大小减 1,排除原来堆顶这个数
swap(arr, 0, --heapSize);
while (heapSize > 0) {
// 交换完之后,调整当前堆顶元素的位置,从新形成一个大根堆,继续这个过程
heapify(arr, 0, heapSize);
// 堆顶位置的数和最后一个数交换,最大值来到了数组的最后,堆大小减 1,排除原来堆顶这个数
swap(arr, 0, --heapSize);
}
}
/**
* heapSize:形成的大根堆的数组元素的数量
* 0 ~ heapSize-1下标位置中 index位置节点的值变小往下沉的过程
* 下降的过程就是 index位置元素和它的左右子节点中较大的交换位置
* @param arr
* @param index
* @param heapSize 当前大根堆的节点数 <= arr.length
*/
public static void heapify(int[] arr, int index, int heapSize) {
// index位置所在节点的左子节点位置
int left = index * 2 + 1;
// 左子节点不越界,也在堆上时
while (left < heapSize) {
// (left+1)为右子节点的下标,如果右子节点不越界 && 左子节点的值 < 右子节点的值
// largest就表示两个子节点中值较大节点在数组中的下标
int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
// 当前节点的值和较大子节点的值比较,返回较大节点值的下标
largest = arr[index] > arr[largest] ? index : largest;
// 如果是本身较大,则不用下沉,此位置就是最终位置
if (largest == index) {
break;
}
// 较大的不是本身,则和较大的节点交换位置
swap(arr, index, largest);
// 修改当前下标为较大节点的下标
index = largest;
// 找当前下标位置对应的左子节点下标
left = 2 * index + 1;
}
}
/**
* 将数组index位置的元素加入到大根堆中
* @param arr
* @param index 数组中当前元素的下标
*/
public static void heapInsert(int[] arr, int index) {
// 如果当前节点大于父节点,交换位置,直到不大于父节点
// 当 index跳到 0位置后,(0-1)/2=0,0位置和 0位置相等,跳出循环
while (arr[index] > arr[(index - 1) / 2]) {
// 当前节点和父节点交换
swap(arr, index, (index - 1) / 2);
// 当前节点跑到父节点的位置继续比较(和交换后的父节点比较)
index = (index - 1) / 2;
}
}
public static void swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
堆的简单应用,求不断加入的数据整体的中位数
四、直接插入排序
public static void insertSort1(int[] arr) {
// 从第二个位置取出每一个数去排序
for (int i = 1; i < arr.length; i++) {
// 从当前数的前一个数开始比较(有序区)
// 如果当前位置的数小于有序区的最后一个数,则交换位置,直到大于等于有序区的某个数,或者比到头,自己最小
for (int j = i - 1; j >= 0; j--) {
if (arr[j + 1] < arr[j]) swap(arr, j, j + 1);
}
}
}
public static void swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
五、直接选择排序
思路:假设第一个最小,从第二个遍历剩下的,遍历到一个比一下,遇到更小的则更新最小的下标,记录最小的下标,遍历完后和第一个位置的数交换,继续以同样的方式搞定除第一个外剩下的数组
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
// 假设第i个数为数组中的最小值
int minIndex = i;
int min = arr[minIndex];
// 从 i后面的数开始循环比较,遇到比 min更小的就赋值给 min
for (int j = i + 1; j < arr.length; j++) {
// 如果碰到比第一个小的就假设这个数为最小
if (arr[j] < min) {
min = arr[j];
minIndex = j;
}
}
// 循环结束找到数组中的最小值,与第一个值进行交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
六、希尔排序
public class ShellSort {
/**
* 交换法希尔排序思路
*/
@Test
public void sortProcess() {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
int temp;
// 第一轮排序,10/2=5,将10个数据分成5组,每组两个元素,同一组的第二个元素在第一个元素后面第五个位置
// 8-3,9-5,1-4,7-6,2-0 则在原顺序中 8-3 9-5 7-6 2-0 需要交换位置
// 因为是两组,所以从下标 5位置开始遍历,即遍历 3,4,5,6,0
// 遍历到的元素分别和组里的元素比较,比的时候使用直接插入的方式
for (int i = 5; i < arr.length; i++) {
//从下标5位置开始,进行插入排序
for (int j = i - 5; j >= 0; j = j - 5) {
//如果当前位置的数小于对应组里的数,就交换位置,然后继续比,直到比到头
if (arr[j + 5] < arr[j]) {
Utils.swap(arr, j, j + 5);
}
}
}
// [3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
System.out.println("第一轮排序:" + Arrays.toString(arr));
// 第二轮排序,继续分组5/2 = 2组 31097 56842
// 从1开始遍历,遍历到就和当前组的元素比较
for (int i = 2; i < arr.length; i++) {
// 从第下标2开始,进行插入排序,不是从前一个位置向前比,而是从前2个位置向前比
for (int j = i - 2; j >= 0; j = j - 2) {
// 当前位置的数小于对应组里的元素,则交换位置继续比(0和1比,比完后和3比)
if (arr[j + 2] < arr[j]) {
Utils.swap(arr, j, j + 2);
}
}
}
// [0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
System.out.println("第二轮排序:" + Arrays.toString(arr));
// 第二轮排序,继续分组2/2 = 1组,此时的过程就是直接插入排序
// 但是这时0已经在前面了,杜绝了直接插入排序,0在最后要比很多次的情况
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0; j = j - 1) {
if (arr[j] > arr[j + 1]) {
Utils.swap(arr, j, j + 1);
}
}
}
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
System.out.println("第三轮排序:" + Arrays.toString(arr));
}
/**
* 交换法
*
* @param arr
*/
public static void shellSortSwap(int[] arr) {
// 10个元素,第一次5组,第二次2组,第三次1组 gap=5,2,1
for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
// 从gap位置开始遍历
for (int i = gap; i < arr.length; i++) {
// 直接插入排序的方式进行排序,从后往前比,步长为gap
for (int j = i - gap; j >= 0; j = j - gap) {
// 如果前一个位置元素大于后一个位置元素,则交换位置
if (arr[j] > arr[j + gap]) {
Utils.swap(arr, j, j + gap);
}
}
}
}
}
@Test
public void shellSortSwap() {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSortSwap(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 移位法,效率更高,速度更快
*
* @param arr
*/
public static void shellSortOffset(int[] arr) {
// 10个元素,第一次5组,第二次2组,第三次1组 gap=5,2,1
for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
// 从gap位置开始遍历
for (int i = gap; i < arr.length; i++) {
// 当前位置
int j = i;
// 当前值
int temp = arr[j];
// 如果当前位置的数小于前面的数
if (arr[j] < arr[j - gap]) {
// 如果没有比到头(j - gap >=o),并且当前值小于前面的值
while (j - gap >= 0 && temp < arr[j - gap]) {
// 当前位置的数替换为前面较大的数
arr[j] = arr[j - gap];
// 当前位置移动到前面位置
j = j - gap;
}
// 此时j在前面位置,前面位置的值替换为当前位置的值
arr[j] = temp;
}
}
}
}
@Test
public void shellSortOffset() {
int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
shellSortOffset(arr);
System.out.println(Arrays.toString(arr));
}
}