排序算法
基本方法,交换和比较:
public abstract class Sort> {
public abstract void sort(T[] nums);
protected boolean less(T v, T w) {
return v.compareTo(w) < 0;
}
protected void swap(T[] nums, int i, int j) {
T tmp = T[i];
T[i] = T[j];
T[j] = tmp;
}
}
选择排序
public class Selection extends Sort< T> {
@Override
public void sort(T[] nums) {
int size = nums.length;
for (int i = 0; i < size; i++) {
int less = i;
for (int j = i; j < size; j++) {
if (less(nums[j], nums[less])) {
less = j;
}
}
swap(nums, i, less);
}
}
}
不稳定。{5,5,2}就不稳定。
插入排序
public class Insertion extends Sort {
@Override
public void sort(T[] nums) {
int size = nums.length;
for (int i = 1; i < size; i++) {
for (int j = i; j > 0 & less(T[j], T[j - 1]); j--) {
swap(nums, j, j - 1);
}
}
}
}
可稳定。等于不再交换,所以可稳定。
冒泡排序
public class Bubble extends Sort {
@Override
public void sort(T[] nums) {
int size = nums.length;
boolean hasSorted = false;
for (int i = 0; i < size && !hasSorted; i++) {
hasSorted = true;
for (int j = 0; j < size - i - 1; j++){
if (less(T[j+1], T[j])) {
hasSorted = false;
swap(nums, j+1, j);
}
}
}
}
}
可稳定。等于不再交换,所以可稳定。
合并排序
方法解析:
https://blog.csdn.net/u010853261/article/details/54894057
public class Merge extends Sort {
@Override
public void sort(T[] nums) {
}
// recursive, from top to bottom.
public void sort(T[] nums, int l, int r) {
if (l >= r>) {
return;
}
int m = (l + r) / 2;
sort(nums, l, m);
sort(nums, m, r);
merge(nums, l, m, r);
}
public void merge(T[] nums, int l, int m, int h) {
T[] tmps = new T[nums.length];
//Arrays.copyOf();
int i = l;
int j = m + 1;
int k = 0;
while (i < m && j < h>) {
if (less(tmps[i], tmps[j])) {
nums[k++] = tmps[i++];
} else {
nums[k++] = tmps[j++];
}
}
while (i < m) {
nums[k++] = tmps[i++];
}
while (j < h) {
nums[k++] = tmps[j++];
}
}
// none recursive. from bottom to top
@Override
public void sort(T[] nums) {
int N = nums.length;
aux = (T[]) new Comparable[N];
for (int sz = 1; sz < N; sz += sz)
for (int lo = 0; lo < N - sz; lo += sz + sz)
merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
可稳定。合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。
快速排序
快速排序几种写法:
https://blog.csdn.net/wusecaiyun/article/details/47862897
int mypartition(vector&arr, i nt low, int high)
{
int pivot = arr[low];//选第一个元素作为枢纽元
int location = low;//location指向比pivot小的元素段的尾部
for(int i = low+1; i <= high; i++)//比枢纽元小的元素依次放在前半部分
if(arr[i] < pivot)
swap(arr[i], arr[++location]);
swap(arr[low], arr[location]);//注意和前面的区别,是为了保证交换到头部的元素比pivot小
return location;
}
void quicksort(vector&arr, int low, int high)
{
if(low < high)
{
int middle = mypartition(arr, low, high);
quicksort(arr, low, middle-1);
quicksort(arr, middle+1, high);
}
}
public class Quick extends Sort {
@Override
public void sort(T[] nums) {
sort(nums, 0, nums.length - 1);
}
public void sort(T[] nums, int l, int r) {
if (l >= r>) {
return;
}
int j = partition(nums, l, r);
sort(nums, i, j - 1);
sort(nums, j + 1, r);
}
public void partition(T[] nums, int l, int r) {
T v = nums[l];
int i = l;
int j = r + 1;
while (true) {
while (less(nums[++i], v) && i != r);
while (less(v, nums[--j]) && j != l);
if (i > j) break;
swap (nums, i, j);
}
swap (nums, l, j);
return j;
}
}
不稳定。因为后边的数字可能会被交换到前边。
如果元素较少,那么采用插入排序可以节省性能。
为避免元素基准点选择不均匀,那么可以采用三数取中作为基准。有关枢纽值的选取有很多种思路,随机选取
public class ThreeMidQuick extends Quick {
@overide
public void sort(T[] nums, int l, int r) {
if (l >= r>) {
return;
}
selectMidToFirst(nums, l, r);
int j = partition(nums, l, r);
sort(nums, i, j - 1);
sort(nums, j + 1, r);
}
private void selectMidToFirst(T[] nums, int l, int r) {
int mid = (l + r) > 2;
if (less(nums, mid, l) && less(nums, l, r)) {
return; //mid->l, no swap.
}
if (less(nums, l, mid) && less(nums, mid, r)) {
swap(nums, l, mid); // mid -> mid, swap(l, mid)
return;
}
swap(nums, l, r); // mid -> r, swap(l, r)
}
}
如果有很多重复的元素,那么可以使用三分法,如下讲数组分拆为三部分,< = >的部分。然后对< 和 > 再进行quicksort即可,如下:
public class ThreeWayQuickSort> extends QuickSort {
@Override
protected void sort(T[] nums, int l, int h) {
if (h <= l)
return;
int lt = l, i = l + 1, gt = h;
T v = nums[l];
while (i <= gt) {
int cmp = nums[i].compareTo(v);
if (cmp < 0)
swap(nums, lt++, i++);
else if (cmp > 0)
swap(nums, i, gt--);
else
i++;
}
sort(nums, l, lt - 1);
sort(nums, gt + 1, h);
}
}
查找数组的第K大元素
快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。
可以利用这个特性找出数组的第 k 个元素。
public T select(T[] nums, int k) {
int l = 0, h = nums.length - 1;
while (h > l) {
int j = partition(nums, l, h);
if (j == k)
return nums[k];
else if (j > k)
h = j - 1;
else
l = j + 1;
}
return nums[k];
}
该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。
堆排序
http://www.cnblogs.com/penghuwan/p/7894728.html
构建堆
从左到右,依次上浮;
从右一半到左, 依次下沉。(更高效, 因为“下沉”需要遍历的节点数比“上浮”需要遍历的节点数少了一半)
int N = nums.length;
for (int i = N/2; i >= 1; i--) {
sink(nums, i, N); //大根堆
}
单个堆节点的有序化有两种情况:
当某个节点变得比它的父节点更大而被打破(或是在堆底加入一个新元素时候),我们需要由下至上恢复堆的顺序
当某个节点的优先级下降(例如,将根节点替换为一个较小的元素)时,我们需要由上至下恢复堆的顺序
实现这两种有序化的操作,分别叫做“上浮”(swim)和“下沉” (sink)
选择元素,每次确定一个N, N-1,然后交换到顶端,从1开始下沉。所以最终只需要使用下沉操作即可。算法如下:
public class HeapSort> extends Sort {
/**
* 数组第0个位置不能有元素
*/
@Override
public void sort(T[] nums) {
int N = nums.length;
for (int i = N/2; i >= 1; i--) {
sink(nums, i, N); //大根堆
}
while (N > 1) {
swap(nums, 1, N--); // 每一次确定一个N, N-1, N-2...1
sink(nums, 1, N);
}
}
private void sink(T[] nums, int k, int N) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && less(nums, j, j + 1))
j++;
if (!less(nums, k, j))
break;
swap(nums, k, j);
k = j;
}
}
private boolean less(T[] nums, int i, int j) {
return nums[i].compareTo(nums[j]) < 0;
}
private void swim(int k) {
while (k > 1 && less(k / 2, k)) {
swap(k / 2, k);
k = k / 2;
}
}
public void insert(Comparable v) {
heap[++N] = v;
swim(N);
}
public T delete() {
swap(nums, 1, N--); // 每一次确定一个N, N-1, N-2...1
sink(nums, 1, N);
}
}
堆排序的复杂度
一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。
对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。
堆排序时一种原地排序,没有利用额外的空间。
现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。
排序稳定性和复杂度问题
- 排序算法的比较
算法 稳定 时间复杂度 空间复杂度 备注
选择排序 no N2 1
冒泡排序 yes N2 1
插入排序 yes N ~ N2 1 时间复杂度和初始顺序有关
希尔排序 no N 的若干倍乘于递增序列的长度 1
快速排序 no NlogN logN
三向切分快速排序 no N ~ NlogN logN 适用于有大量重复主键
归并排序 yes NlogN N
堆排序 no NlogN 1
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 ~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
- Java 的排序算法实现
Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。
稳定行分析:
https://blog.csdn.net/DeepLies/article/details/52593597
https://blog.csdn.net/weiwenhp/article/details/8621049
其他种排序思路
https://www.cnblogs.com/ECJTUACM-873284962/p/6935506.html#autoid-1-1-0
https://www.cnblogs.com/ttltry-air/archive/2012/08/04/2623302.html
桶排序
分成若干个有序的区间桶,每个区间单独排序,然后串联起来即可。
[图片上传失败...(image-eb7532-1533265888436)]
基数排序
看基数,比如10进制数那么就有10个数组链表,类似hashmap,然后进行个位数,十位数,百位数...等k次全数组遍历,每次都从0-10数组链表开始遍历,每个万位到最大数即可。
1 pass #0: 170 45 75 90 2 24 802 66
2 pass #1: 170 90 2 802 24 45 75 66
3 pass #2: 2 802 24 45 66 170 75 90
4 pass #3: 2 24 45 66 75 90 170 802