归并排序
归并排序算法的核心思想是,先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并到一起,这样整个数组就是有序的了
mergeSort(p…r) = merge(mergeSort(p…q), mergeSort(q+1…r)) 终止条件:p >= r 不能再继续分解
merge数组A, B 部分的过程中,先申请一个临时数组 help ,用两个游标 q1, q2 分别指向A和B的第一个元素,比较A[q1] 和 B[q2], 如果A[q1] < B[q2], 就把A[q1]存入 help 数组, q1++, 否则将 B[q2] 存入 help, q2++
当遇到两个子数组其中一个全部存入 help 中时, 把另一个数组的中的数据依次放到 help 的末尾.这时临时数组中的数据就是有序状态, 之后再把 help 复制到原数组中.
性能分析: 归并排序是一个稳定的排序算法(由 merge 中的比较方式实现), 执行效率与原始数据的有序度无关, 时间复杂度是 O(nlogn), 由于要创建临时数组, 故空间复杂度 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 q1 = l; //浮动指针
int q2 = m + 1;
while (q1 <= m && q2 <= r) {
help[i++] = arr[q1] <= arr[q2] ? arr[q1++] : arr[q2++];
}
while (q1 <= m) { //q1剩余
help[i++] = arr[q1++];
}
while (q2 <= r) { //q2剩余
help[i++] = arr[q2++];
}
//复制到原数组
for (i = 0; i < help.length; i++) {
arr[l++] = help[i];
}
}
快速排序
原理:
如果要排数组中 l 到 r 之间的一组数据, 我们选择l 到 r 之间的任意一个元素作为 分区点[pivot], 将大于 pivot 的数据分到右边, 将小于 pivot 的数据分到左边, 将 pivot 分到中间. 再对左边, 右边分别递归分区, 直到区间缩小为1, 即l == r (递归终止条件).
partition 中通过游标 l 把数组分为两部分, 已处理区间和未处理区间, 其中 less --> l 为pivot
递推公式: quickSort(p…r) = quickSort(p…q-1) + quickSort(q + 1…r)
性能分析: 由于交换的时候会改变相同元素的相对位置, 所以快排是不稳定的排序算法, 通过分区函数 partition, 实现原地排序, 不占用大量内存, 这一点优于归并排序, 空间复杂度O(logn 指的是递归深度, 要用到栈空间)
最坏情况下, 数据已经有序, 则会造成分区不均等, 分 n 次区, 时间复杂度为O(n^2)
最好情况下, 正好平均分区, 为O(nlogn)
平均时间复杂度O(nlogn)
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; //右边界 //把最后一个元素arr[r]作为 pivot
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
} //此时 less 和 l 之间是pivot
} //l == more
swap(arr, more, r); //把比较对象放到中间
return new int[] {less + 1, more}; //返回 pivot 的区间
}
过程图
快排的一个应用--如何在O(n)时间复杂度内求无序数组中第K大元素
例如:arr = {6, 1, 3, 5, 7, 2, 4, 9, 11, 8} //10个数
把最后一个数8作为pivot, 一遍快排后为:
11, 9, 8 , 7, 2, 4, 3, 1, 6, 5
数组分为3部分, arr[0…p-1], arr[p], arr[p+1…r]
此时看pivot左边有2个元素就可以知道pivot为第2+1大的元素
-
如果p+1 == k, 则找到
-
如果p+1 > k, 则表示第k大元素在左边,需要对[0, p-1]再次分区
-
如果p+1 < k, 则表示第k大元素在右边,需要对[p+1, r]再次分区
代码:
public static void foundK(int[] arr, int k) {
if (arr == null || arr.length < k || k == 0) {
System.out.println("error");
return;
}
Partition(arr, 0, arr.length - 1, k);
}
public static void Partition(int[] arr, int l, int r, int k) {
int left = l;
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);
if ((less + 2) == k) {
System.out.println("第" + k + "大元素是" + arr[less + 1]);
} else if ((less + 2) < k) {
//在右边, 对右边再次分区
Partition(arr, less + 2, r, k);
return;
} else {
//在左边,对左边再次分区
Partition(arr, left, less, k);
return;
}
}
二分查找
依稀记得大三上学期, 上算法分析课的时候, 黄大爷跟我们说"你们这些人当中还有的人连二分查找都不会写的", 脑瓜一想, 我擦这不就说的我嘛哈哈哈哈
二分查找的时间复杂度是 O(logn), 查找的数组要求有序.
适用于顺序存储结构, 一经建立就很少改动, 而又经常需要查找的线性表.
(对那些查找少而又经常需要改动的线性表,可采用链表作存储结构)
public static int binarySearch(int[] arr, int key) {
if (arr == null || arr.length < 1) return -1;
int l = 0;
int r = arr.length - 1;
int m;
while (l <= r) {
m = l + ((r - l) >> 1);
if (key == arr[m]) {
return m;
} else if (key < arr[m]) {
r = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
二分查找的链表实现?