目录
1、初级排序
1.1选择排序
总体思想:遍历数组,当前数字和剩余数字逐个对比,选择剩余数字中最小的元素和当前数字交换位置。
时间复杂度:o(n^2)
空间复杂度:o(1)
1.2 插入排序
总体思想:前面的数组是有序的,插入一个元素的时候,对有序的数组从后往前遍历,将比要插入的数大的数字往后移动一位,直到找到第一个比他小的数,就可以把这个数插入序列。
代码:
public static void sortColors(int[] nums) {
//插入排序
for (int i = 1; i < nums.length; i++) {
// 将比nums[i]大的数放到其之后
for (int j = i; j > 0 && nums[j] < nums[j - 1]; j--) {
// 只要前面的比当前位置大,就交换两者
swap(nums, j, j-1);
}
}
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
}
时间复杂度:
最好 : 直接排好序了, O(n)
最坏 :每次都需要移动,移动 1 + 2 + 3 +…… + n = O(n^2)
2、快速排序
- 总体思想:分治,在某个切分元素的左右边元素分别排序,从而数组整体排序。
优点:1、原地排序,只需要很小的辅助栈空间复杂度低;2、时间复杂度o(NlgN);
缺点:脆弱
- 代码
快排递归地将子数组a[low .. hig] 排序,首先用partition()方法将某个元素a[j]放到合适位置,再递归地调用将其他位置的元素排序。partition()方法使得数组满足三个条件:
(1)对于某个j,a[j] 排定;
(2)a[low] 到 a[j-1] 的所有元素小于等于a[j];
(3)a[j+] 到 a[hig] 的所有元素小于等于a[j];
切分思路:
1、随机选取a[low] 为切分元素v;
2、数组从左往右扫描直到找到大于等于切分元素v的元素,从右往左扫描直到找到小于等于切分元素v的元素;
3、这两个元素没有排定,交换其位置,比v小的在前,大的在后;
4、重复步骤2、3,保证左指针i左边元素小等于v,右指针j右边元素大等于v;
5、直到两个指针相遇,将切分元素v和左子数组最右侧元素a[j]交换,返回切分元素最后位置j。
partition()方法代码:
// ---partition切分-----
public static int partition(int[] a, int low, int hig) {
// 将数据切分为a[low .. i-1] , a[i] , a[i+1 .. hig]
int i = low, j = hig + 1;
int v = a[low];
while (true) {
// 扫描 + 交换
while(a[++i] < v) {
if (i == hig) break;
}
while (a[--j] > v) {
if (j == low) break;
}
if (i >= j) break;
swap(a, i, j);
}
swap(a, low, j);
return j;
}
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
完整快排:
package cn.txt.arrays;
import java.util.Arrays;
/**
* 快排
* @author tyler
*
*/
public class QuickSort {
// --- 递归实现快排-----
public static void quickSort(int[] a) {
quickSortCore(a, 0, a.length-1);
}
public static void quickSortCore(int[] a, int low, int hig) {
if (hig < low) {
return;
}
int j = partition(a, low, hig);
quickSortCore(a, low, j-1);
quickSortCore(a, j+1, hig);
}
// ---partition切分-----
public static int partition(int[] a, int low, int hig) {
// 将数据切分为a[low .. i-1] , a[i] , a[i+1 .. hig]
int i = low, j = hig + 1;
int v = a[low];
while (true) {
// 扫描 + 交换
while(a[++i] < v) {
if (i == hig) break;
}
while (a[--j] > v) {
if (j == low) break;
}
if (i >= j) break;
swap(a, i, j);
}
swap(a, low, j);
return j;
}
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
int[] te = {7,2,5,4,9,5,6,8};
quickSort(te);
System.out.println(Arrays.toString(te));
}
}
时间复杂度:
最好:每次patition都分的很均匀,每次都分在序列的中间位置,那么需要递归logn次,每次递归的时间复杂度为n,总共O(n * logn);
最坏:最坏情况是每次都划分成一个元素和其他元素,需要递归n次,一次递归的时间复杂度是n,因此T(n)=n^2。
3、归并排序
- 总体思想:
归并排序算法的关键操作是"合并"步骤中两个已排序序列的合并。
我们可以通过调用一个辅助过程Merge(A, p, q, r)来完成合并,其中A是一个数组,p、q和r是数组下标,满足 p ≤ q < r。该过程假设子数组A[p..q]和A[q+1..r]都已排好序。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组A[p..r]。
- 时间复杂度
对长度为n的文件,需进行lgn趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
- 空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n)
- 代码实现
参见算法第四版p170。主要包含两部分:
- 原地归并函数merge(int[] a , int low, int mid, int hig):将a[low ... mid] 和 a[mid ... hig] 归并为一个有序数组,存放在a[low ... hig]中;
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤3直到某一指针达到序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾
// merge函数
public static void merge(int[] a, int low, int mid, int hig) {
// 原地归并a[low ... mid] 和 a[mid ... hig]为有序数组
int i = low, j = mid + 1;
// 复制数组到辅助空间
for (int k = low; k <= hig; k++) {
aux[k] = a[k];
}
// 排序 + 归并
for (int k = low; k <= hig; k++) {
if (i > mid) {
// 左半用尽,取右
a[k] = aux[j++];
} else if (j > hig) {
// 右半用尽,取左
a[k] = aux[i++];
} else if (aux[i] > aux[j]) {
// 右小,取右
a[k] = aux[j++];
} else {
// 左小,取左
a[k] = aux[i++];
}
}
}
- 基于不同方向的归并排序:自上向下、自下向上
1、自上而下: 基于原地归并,应用分治思想,递归地解决问题。如果将两个子数组排序,则能够通过归并两个子数组将整个数组排序。
调用的动态情况见书p172,递归地先后对左右子数组排序,然后归并。
时间复杂度:o(NlgN), 最多访问长度为N的数组6NlgN次。
(1)拆分
(2)递归排序合并
// -------自上而下归并---------------
public static void sort(int[] a) {
// 一次性分配空间
aux = new int[a.length];
// 递归部分,归并核心
sortCore(a, 0, a.length - 1);
}
private static void sortCore(int[] a, int low, int hig) {
// 排序a[low, hig]
if (hig <= low) {
return;
}
int mid = low + (hig - low) / 2;
sortCore(a, low, mid);
sortCore(a, mid + 1, hig);
merge(a, low, mid, hig);
}
2、自下而上:分治思想,循序渐进地解决问题。先归并微型数组,再成对归并得到的子数组。这种方式比标准递归代码量更少。
多次遍历整个数组,根据子数组大小进行两两归并。子数组大小sz初始值为1,每次加倍,最后一个子数组的大小是sz的偶数倍的时候才会等于sz,否则比sz小。
时间复杂度:和自上而下归并一样,o(NlgN)
适用:适合链表组织的数据,著需要重新组织链表链接就能将链表原地排序,不需要创建任何新的链表节点。
// ---- 自下而上归并---------------
public static void sortDoUp(int[] a) {
// lgN次两两归并
int N = a.length;
aux = new int[N];
for (int sz = 1; sz < a.length; sz = sz + sz) { // sz子数组大小
for (int lo = 0; lo < N - sz; lo += sz + sz) { // lo子数组索引
merge(a, lo, lo + sz + 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
3、完整归并排序:
package cn.txt.arrays;
import java.util.Arrays;
/**
* 归并排序
*
* @author tyler
*
*/
public class MergeSort {
// 归并所需辅助数组
private static int[] aux;
// ---- 自下而上归并---------------
public static void sortDoUp(int[] a) {
// lgN次两两归并
int N = a.length;
aux = new int[N];
for (int sz = 1; sz < a.length; sz = sz + sz) { // sz子数组大小
for (int lo = 0; lo < N - sz; lo += sz + sz) { // lo子数组索引
merge(a, lo, lo + sz + 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
// -------自上而下归并---------------
public static void sort(int[] a) {
// 一次性分配空间
aux = new int[a.length];
// 递归部分,归并核心
sortCore(a, 0, a.length - 1);
}
private static void sortCore(int[] a, int low, int hig) {
// 排序a[low, hig]
if (hig <= low) {
return;
}
int mid = low + (hig - low) / 2;
sortCore(a, low, mid);
sortCore(a, mid + 1, hig);
merge(a, low, mid, hig);
}
// merge函数
public static void merge(int[] a, int low, int mid, int hig) {
// 原地归并a[low ... mid] 和 a[mid ... hig]为有序数组
int i = low, j = mid + 1;
// 复制数组到辅助空间
for (int k = low; k <= hig; k++) {
aux[k] = a[k];
}
// 排序 + 归并
for (int k = low; k <= hig; k++) {
if (i > mid) {
// 左半用尽,取右
a[k] = aux[j++];
} else if (j > hig) {
// 右半用尽,取左
a[k] = aux[i++];
} else if (aux[i] > aux[j]) {
// 右小,取右
a[k] = aux[j++];
} else {
// 左小,取左
a[k] = aux[i++];
}
}
}
public static void main(String[] args) {
int[] te = { 7, 2, 5, 4, 9, 5, 6, 8 };
sort(te);
System.out.println(Arrays.toString(te));
}
}
4、优先队列
4、1 堆排序
1、堆有序:一棵二叉树的父节点大等于其子节点
2、二叉堆:一组能够用堆有序的完全二叉树排序的元素,在数组中按层存储,不使用数组的第一个位置,根节点在位置1,其子节点在位置2,3,依此类推。
(1)节点k的父节点:k/2
(2)节点k的子节点:2k、2k+1
(3)在不使用指针的情况下,通过数组索引在树中上下移动
(4)大小为N的完全二叉树高度为lgN
3、完整堆排序:
for循环构造堆,while循环将最大元素a[0]和a[N]交换并修复堆,如此重复直到堆变空。
package cn.txt.arrays;
import java.awt.RenderingHints.Key;
import java.util.Arrays;
/**
* 堆排序
* @author tyler
*
*/
public class heapSort {
// 基于堆的完全二叉树,满足堆有序,不使用指针在树中上下移动
// --- 堆排序-----------------------------
public static void heapSortCore(int[] a) {
int N = a.length-1;
// 从右到左使用sink函数构造子堆,并且下沉排序
for (int k = N/2; k >= 0; k--) {
sink(a, k, N);
}
while (N > 0) {
swap(a, 0, N--);
sink(a, 0, N);
}
}
//--- 堆有序化,下沉实现,小的往下沉直到满足堆有序--------
private static void sink(int[] a, int k,int N) {
while (2*k <= N) {
int j = 2*k;
if (j < N && a[j] < a[j+1]) j++;
if (a[k] >= a[j]) break;
swap(a, k, j);
k = j;
}
}
// // --- 堆有序化,上游实现,大的往上游直到满足堆有序----
// private static void swim(int[] a, int k) {
// int N = a.length;
// while (k > 1 && a[k/2] < a[k]) {
// swap(a, k, k/2);
// k = k/2;
// }
// }
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
int[] te = {7,2,5,4,9,5,6,8};
heapSortCore(te);
System.out.println(Arrays.toString(te));
}
}
5、手撕冒泡排序
- 总体思想:遍历数组,一次比较两个相邻元素,如果顺序错误则交换顺序,逐步将较大的元素“冒”到数组末尾。
- 步骤:
1、比较相邻元素,前大后小则交换;
2、遍历数组,从头到尾,知道最后一对,此时最后一位为最大值;
3、对除了最后一位的剩余数组重复以上步骤,直到比较完所有数字。
public static int[] BubbleSortSolution(int[] nums) {
// 异常
if (nums == null || nums.length < 0) {
return nums;
}
int temp = 0;
for (int i = 0; i < nums.length-1; i++) {
for (int j = 0; j < nums.length-1-i; j++) {
// 内循环,比较交换,较大的数放在后面
if (nums[j] > nums[j + 1] ) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] =temp;
}
}
}
return nums;
}
时间复杂度:o(n^2)
空间复杂度:o(1)