控制台运行编译java程序带中文乱码问题解决办法: javac -encoding utf-8 Test.java
稳定性:排序算法需要保留数组中重复元素的相对位置。(具体详见算法第四版P217)
冒泡排序
思想: 两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
时间复杂度: O(N2)
空间复杂度: O(1)
稳定性: 稳定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
for (int i = 0; i < N-1; i++) {
for (int j = 0; j < N-i-1; j++) {
if (nums[j] > nums[j + 1]) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
}
选择排序
思想: 每一趟在n-i+1(i=1,2,…,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录。
时间复杂度: O(N2)
空间复杂度: O(1)
稳定性: 不稳定(说明:举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法)
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
for (int i = 0; i < N; i++ ) {
int min = i;
for (int j = i+1; j < N; j++) {
if (nums[j] < nums[min]) {
min = j;
}
}
if (min != i) {
temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
}
}
插入排序
思想: 将一个记录插入到已经排序好的有序表中,从而得到一个新的、记录数增1的有序表
时间复杂度: O(N2)
空间复杂度: O(1)
稳定性: 稳定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
for (int i = 1; i < N; i++ ) {
for (int j = i; j > 0 && nums[j] < nums[j-1]; j--) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
改进版本
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int i = 0;
int j = 0;
for (i = 1; i < N; i++) {
if (nums[i] < nums[i-1]) {
int temp = nums[i];
for (j = i-1; (j >= 0) && (nums[j] > temp); j--) {
nums[j+1] = nums[j];
}
nums[j+1] = temp;
}
}
}
}
希尔排序(插入排序升级)
思想: 将数据分为若干组记录,然后分别对每一组做插入排序。
时间复杂度: O(NlgN)
空间复杂度: O(1)
稳定性: 不稳定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
int h = 1;
int i = 0;
int j = 0;
//固定步长:1,4,13,40...
while (h < N/3) {
h = 3*h + 1;
}
while (h >= 1) {
//下面操作和插入排序算法基本相同
for (i = h; i < N; i++) {
if (nums[i] < nums[i - h]) {
int temp = nums[i];
for (j = i-h; (j >= 0) && (nums[j] > temp); j = j-h) {
nums[j+h] = nums[j];
}
nums[j+h] = temp;
}
}
h = h / 3;
}
}
}
快速排序(冒泡排序增强)
思想: 选取一个轴值(比较的基准),将待排序记录分为独立的两个部分,左侧记录都是小于或等于轴值,右侧记录都是大于或等于轴值,然后分别对左侧部分和右侧部分重复前面的过程,也就是左侧部分又选择一个轴值,又分为两个独立的部分,这就使用了递归了。到最后,整个序列就变得有序了。
时间复杂度: O(NlgN)
空间复杂度: O(lgN)~O(N)
稳定性: 不稳定
public class Sort {
public static void sort(int[] nums) {
//消除对输入的依赖
//StdRandom.shuffle(a);
sort(nums, 0, nums.length - 1);
}
public static void sort (int[] nums, int lo, int hi) {
if (hi <= lo) return;
//切分
int j = partition(nums, lo, hi);
sort(nums, lo, j-1);
sort(nums, j+1, hi);
}
public static int partition(int[] nums, int lo, int hi) {
//定义左右扫描的指针
int i = lo, j = hi + 1;
//定义切分的元素d
int point = nums[lo];
int temp = 0;
while (true) {
while (nums[++i] < point) if (i == hi) break;
while (point < nums[--j]) if (j == lo) break;
if (i >= j) break;
//交换两个元素
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
//将切分元素放到数组中合适位置
temp = nums[lo];
nums[lo] = nums[j];
nums[j] = temp;
return j;
}
}
堆排序(选择排序增强)
思想: 利用优先队列的删除最大元素的特点,依次将将删除的最大元素保存起来,就有序了。当然,这里的优先队列是用堆实现的。
第一步:需要保证堆有序,也就是每一个父节点要比它的任意子节点要大;
第二步:使用堆的下沉操作来实现排序;
时间复杂度: O(NlgN)
空间复杂度: O(1)
稳定性: 不稳定
public class Sort {
public static void heapSort(int[] a) {
int N = a.length;
//第一步:通过下沉操作来实现堆有序,这里只需要操作数组中前面的一半元素即可
for (int k = N / 2; k >= 1; k--) {
sink(a, k, N);
}
//第二步:将堆有序的数组使用下沉操作来得到有序数组
while (N > 1) {
exch(a, 1, N--);
sink(a, 1, N);
}
}
//堆下沉操作
public static void sink(int[] a, int k, int N) {
while (2*k <= N) {
int j = 2*k;
//取两个子节点中较大的一个
if (j < N && less(a, j, j+1)) j++;
//比较如果父节点比子节点中较大的一个小,则交换
if (!less(a, k, j)) break;
exch(a, k, j);
//继续往下面遍历
k = j;
}
}
//之所以取i-1,是因为堆中下标是从1开始的,需要还原到数组中从0开始的。
public static boolean less(int[] a, int i, int j) {
return a[i-1] < a[j-1];
}
public static void exch(int[] a, int i, int j) {
int temp = a[i-1];
a[i-1] = a[j-1];
a[j-1] = temp;
}
}
归并排序
思想: 要将一个数组排序,可以先(递归地)将它们分成两半分别排序,然后将它们的结果归并起来。
时间复杂度: O(NlgN)
空间复杂度: O(N)
稳定性: 稳定
public class Sort {
//1.将两个有序数组合并为一个有序数组
public static void merge(int[] a, int lo, int mid, int hi) {
//将a[lo..mid] 和 a[mid+1..hi]归并
//i代表左半边索引,j代表右半边索引
int i = lo, j = mid + 1;
//定义一个辅助数组
int[] aux;
//将a[lo..hi]复制到aux[lo..hi]
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
//左半边元素用尽,取右半边的元素
if (i > mid) {
a[k] = aux[j++];
//右半边元素用尽,取左半边元素
} else if (j > hi) {
a[k] = aux[i++];
//右半边的当前元素小于左半边的当前元素,则取右半边的元素
} else if (aux[j] < aux[i]) {
a[k] = aux[j++];
//左半边的当前元素小于等于右半边的当前元素,则取左半边的元素
} else {
a[k] = aux[i++];
}
}
}
//自顶向下
pubic static void sort (int[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int mid = lo + (hi - lo) / 2;
//将左半边排序
sort(a, lo, mid);
//将右半部分排序
sort(a, mid+1, hi);
//归并结果
merger(a, lo, mid, hi);
}
//自底向上
public static void sort (int[] a) {
int N = a.length;
int[] aux = new int[N];
//定义子数组的大小
for (int sz = 1; sz < N; sz = sz + sz) {
//子数组的索引
for (int lo = 0; lo < N-sz; lo += sz+sz) {
//lo+sz+sz-1可能越界,因此需要一个min函数来取边界
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
}
}
}
总结
时间复杂度:
O(N)
计数排序、基数排序
O(N2)
冒泡排序、选择排序、插入排序
O(NlogN)
希尔排序、堆排序、快速排序、归并排序
空间复杂度:
O(1)
插入排序、选择排序、冒泡排序、堆排序、希尔排序
O(logN) ~ O(N)
快速排序
O(N)
归并排序
O(M) M为桶的数量
计数排序、基数排序
稳定性:
稳定的排序算法:
冒泡排序、插入排序、归并排序、计数排序、基数排序、桶排序
不稳定的排序算法:
选择排序、快速排序、希尔排序、堆排序