插入元素的排序:每次待排元素是以插入形式排好的,就和打扑克牌一样。
属于插入排序的有:直接插入排序,希尔排序(shell排序)。
选择排序:选择最大的或者最小的,然后放到尾部或者首部。
属于选择排序的有:选择排序,堆排序。
交换排序:排序时需要交换元素的排序。
属于交换排序的有:冒泡排序,快速排序
归并排序:将两个有序的序列合并成一个有序的序列
属于归并排序的有:归并排序
如果Ai == Ai+1,前一个元素等于后一个元素,当排序完成之后,Ai和Ai+1的相对位置不变,则称这个排序为稳定的排序。否则为不稳定的排序。
稳定的排序:
冒泡排序,由于冒泡排序对于相邻的两个元素相等时不会交换顺序,所以,冒泡排序是一种稳定的排序。
插入排序,由于插入排序,插入的时候是按照元素的先后顺序插入的(从1 到n - 1),而对于相等的两个元素,直接放在前一个元元素的后面,所以插入排序两个相同元素的前后位置是不会改变的。所以插入排序是稳定的排序。
归并排序,两个相等的元素,拆分和归并之后的相对顺序不会改变。所以是稳定的排序
基数排序和计数排序,两个相等的元素入桶和出桶顺序都是一样的(出入桶相当于队列),不会交换。所以基数排序和计数排序是稳定的排序。
不稳定的排序:
选择排序,由于选择排序对于相邻的两个元素大小相等时,选择前一个为min就不会选择后面的了,这样相对顺序就改变了,
所选择排序不是一个稳定的排序。
希尔排序,由于希尔排序是按照不同间隔对元素进行插入排序(插入排序元素之间的间隔为1,希尔排序间隔是k逐渐递减)。
所以,即使两个元素相邻,但是由于他们在不同的间隔里面,会在各自的间隔里面随机移动。所以希尔排序不稳定。
快速排序,快速排序的不稳定发生在key和中间元素交换的时候,例如序列为3 4 3 8 6 8 7 9 5,5会和第一个8交换,所以破坏了稳定性。所以快速排序不稳定。
堆排序,在排序时如果parrent的两个子节点相等,在堆排时,会将左孩子调整到堆顶,这样就破坏了他们的相对位置。所以堆排序不稳定。
冒泡排序:
外层循环控制比较层数,内层循环控制比较次数。使用一个finish来标记是否已经有序了。
还可以优化的是,每次从左向右找都找到最小的,和最大的,最小的放到第一个位置,最大的放到最后一个位置。
public static void bubbleSort(int[] arr) {
for(int i = 0, len = arr.length; i < len; ++i) {
boolean finish = true;
for(int j = 0; j < len - i - 1; ++j) {
if(arr[j] > arr[j + 1]) {
finish = false;
Util.swap(arr, j, j + 1);
}
}
if(finish)
break;
}
}
选择排序:
每次都从序列中选一个最大的和一个最小的,然后将最大的和‘最后一个’交换,最小的和‘第一个’交换。
特例是当min为’最后一个元素‘时,先交换max会将min所指向的元素换走。或者先交换min的时候,max为‘第一个’元素,
public static void selectSort(int[] arr) {
int len = arr.length;
int max = 0;
int min = 0;
for(int i = len - 1; i >= 0; --i) {
min = len - 1 - i;
max = i;
if(max == min)
break;
for(int j = len - 1 - i; j <= i; ++j) {
if(arr[j] > arr[max])
max = j;
if(arr[j] < arr[min])
min = j;
}
if(min != len - 1 - i) {
Util.swap(arr, min, len - 1 - i);
if(max == len - 1 - i)
max = min;
}
if(max != i) {
Util.swap(arr, max, i);
}
}
}
插入排序:
第一个元素默认有序,然后从第二个元素起,开始向前比较如果遇到的元素比这个元素小,就将这个元素插到遇到的元素后面
这个函数中gap是1。设置gap是用来给shell排序用的。
public static void insertSort(int[] arr, int gap) {
int len = arr.length;
for(int i = gap; i < len; i += gap) {
int key = arr[i];
while(i - gap >= 0 && arr[i - gap] > key) {
arr[i] = arr[i - gap];
i -= gap;
}
arr[i] = key;
}
}
希尔排序(shell排序):
希尔排序是插入排序的变种,设置一个间隔,然后对间隔这么多的元素进行排序。然后这个间隔逐渐减小,直到减少到0。
// 希尔排序,gap是变化的
public static void shellSort(int[] arr) {
int len = arr.length;
for(int gap = len/2 + 1; gap > 0; --gap) {
insertSort(arr, gap);
}
}
堆排序:
什么是堆?
利用堆顶元素为最大或者最小的特点,每次将堆顶元素和最后一个元素交换,然后对这n - 1个元素进行向下调整。继续进行
第一个元素和倒数第二个元素进行交换,然后对着n - 2个元素进行向下调整,直到n - 1== 0结束排序。升序使用大堆,降序使用小堆。
建堆:
// 建堆
public static void buildHeap(int[] arr) {
int len = arr.length;
int parrent = (len - 2) >> 1; //第一个非叶子节点
while(parrent >= 0) {
adapter(arr, len, parrent);
--parrent;
}
}
向下调整:
// 向下调整, len为开区间
public static void adapter(int[] arr, int len, int parrent) {
int child = 0;
while(true) {
child = parrent * 2 + 1;
if(child >= len)
break;
// 找最大的孩子
if(child + 1 < len && arr[child + 1] > arr[child])
++child;
// 判断最大的孩子是否大于parrent
if(arr[parrent] < arr[child]) {
Util.swap(arr, parrent, child);
parrent = child;
} else {
break;
}
}
}
开始排序:
// 堆排序
public static void heapSort(int[] arr) {
buildHeap(arr);
for(int i = arr.length - 1; i > 0; --i) {
Util.swap(arr, 0, i);
adapter(arr, i, 0);
}
}
快速排序:
快速排序有三种实现方法,但是步骤只有三步。
第一步为快排的核心(分组),将序列分为两组,左边的比序列大,右边的比序列小。
第二步,对左边再进行第一步。
第三步,对右边再进行第一步。
对于第一步有三种实现方法。
这里以升序为例
方法1:
最常见的方法:内聚法,先随便设置一个元素为key(例如right所在位置的元素),然后在当前待排序列的最左边设置一个begin标记,最右边设置一个end标记,begin++如果左边遇到了比key大的,停下,然后end--,如果右边遇到了比key小的。交换begin和end所指向的元素,直到begin>= end。退出此次排序,然后交换key和begin所在位置的元素,此时begin和end一定是相等的。
以下是实现。
/**
* 聚合法,同时从两端开始找,左边找到大于key的,右边找到小于key的,然后交换这两个元素
* @param arr 当前待排数组
* @param left
* @param right
* @return
*/
public static int split3(int[] arr, int left, int right) {
int begin = left;
int end = right - 1;
int key = arr[end];
while(begin < end) {
// 向后找比key大的
while(begin < end && arr[begin] <= key) {
++begin;
}
// 向前找比key小的
while(begin < end && arr[end] >= key) {
--end;
}
// 交换两个找到的元素
if(begin < end)
Util.swap(arr, begin, end);
}
Util.swap(arr, begin, right - 1); // 将标号所在元素居中
return begin;
}
方法2:
挖坑法:
和内聚法一样,不同的是,挖坑法是先将key所在位置设置为坑,然后左边设置一个begin标记,右边设置一个end标记。如果左边遇到了比key大的,就将左边这个元素放到坑,里面,然后left的当前位置设置为坑,然后end--,如果遇到了比key小的,就放到坑里面,然后将当前位置设置为坑。一直循环,直到begin == end;最后将key赋值给最后一个坑,就是begin所在的位置。
/**
* 挖坑法
* 先将right所在位置设置为坑,并记录他的元素大小,作为一个分界(key),
* 然后left向右找,如果遇到了比key大的,则把当前元素赋值给坑,并把当前位置设置为坑,然后right向左找,如果遇到了
* 比key小的,就把当前元素赋值给坑,然后把当前位置设置为坑。
*/
public static int split2(int[] arr, int left, int right) {
int begin = left;
int end = right - 1;
int key = arr[end];
while(begin < end) {
// 向后找
while(begin < end && key > arr[begin]) {
++begin;
}
// 当前位置设置为坑,然后把坑填住,并不再访问这个元素。
if(begin < end) {
arr[end] = arr[begin];
--end;
}
// 向前找
while(begin < end && key < arr[end]) {
--end;
}
// 当前位置设置为坑,然后把坑填住,并不再访问这个元素。
if(begin < end) {
arr[begin] = arr[end];
++begin;
}
}
arr[begin] = key;
return begin;
}
方法3:
前后标记法:
设置两个标记,一个cur标记表示当前位置,一个pre总是标记大于key的前一个小于key的元素。
/**
* 前后指针法,cur是当前指针,pre总是指向小于key的元素,pre的下一个一定是大于key的元素,cur一直向后找,如果遇到比key小的,
* ++cur, ++pre
* 如果遇到比key大的,就只是cur++,pre不变,如果cur再次遇到比key小的,如果pre不等于cur,就交换pre的下一个元素和cur
* @param arr
* @param left
* @param right
*/
public static int split1(int[] arr, int left, int right) {
int cur = left;
int pre = left - 1;
int key = arr[right - 1];
while(cur < right) {
// 看当前元素是否大于
if(arr[cur] < key && ++pre != cur) {
Util.swap(arr, cur, pre);
}
++cur;
}
// 交换pre的下一个和key
if(++pre < right) {
Util.swap(arr, pre, right - 1);
}
return pre;
}
对上述方法的调用:
public static void quickSort(int[] arr, int left, int right) {
// 大于一个元素才排序
if(right - left > 1) {
int mid = split3(arr, left, right);
quickSort(arr, left, mid);
quickSort(arr, mid + 1, right);
}
}
使用挖坑法和内聚法时快排最后总是有left == right,所以最终是使用left或者right作为中间元素赋值其实都是一样的。
对于快速排序的优化:快速排序如果想要排的是顺序,但是元素是倒序的,时间复杂度就会下降到O(n^2),所以每次对于key的选择是非常重要的,如果每次都选择最小的元素的话,每次调整都没有效果,所以对于快速排序的优化主要是对找key元素的优化。取三个元素left mid right, 三个位置的元素,取他们中大小居中的元素。这样每次取到最小元素或者最大元素的概率就会变小。
归并排序:
使用递归将元素拆分为n个1个元素的序列,再将每一对序列(前后两个元素)按照归并的规则合并。继续归并一对元素都是2个已排好序的序列。
public class Test {
public static void mergeSort(int[] arr) {
int len = arr.length;
int left = 0, right = len;
int[] tmp = new int[len];
_mergeSort(arr, left, right, tmp);
}
/**
* 对数据进行分组
* @param arr 数据
* @param left
* @param right
* @param tmp 临时数组
*/
public static void _mergeSort(int[] arr, int left, int right, int[] tmp) {
if(right - left > 1) {
// 先求mid
int mid = left + ((right - left) >> 1);
// 将左边的数据进行分组
_mergeSort(arr, left, mid, tmp);
// 将右边的数据进行分组
_mergeSort(arr, mid, right, tmp);
// 合并左右两个分好组的数据
mergeData(arr, left, mid, right, tmp);
// 将tmp数组拷贝给arr
copyArray(tmp, arr, left, right);
}
}
/**
* 对两区间之间的数据进行合并,然后放到tmp数组里面去
* @param arr 待排序列
* @param left 左区间的左边界
* @param mid 左区间的右边界,不包含,作为右区间的左边界时包含
* @param right 右区间的右边界
* @param tmp 存放当前排好的序列。
*/
public static void mergeData(int[] arr, int left, int mid, int right, int[] tmp) {
int len = tmp.length;
int i = left;
int cur = mid;
while(i < len) {
if (arr[left] <= arr[cur]) {
tmp[i++] = arr[left++];
} else {
tmp[i++] = arr[cur++];
}
if(left == mid) {
while(cur < right) {
tmp[i++] = arr[cur++];
}
break;
}
if(cur == right) {
while(left < mid) {
tmp[i++] = arr[left++];
}
break;
}
}
}
}
归并排序的非递归实现:
省去了拆分的步骤,直接从最后一个元素开始归并。
// 非递归版本的归并排序
public static void mergeSortNor(int[] arr) {
// 归并间隔
int gap = 1;
int len = arr.length;
int[] tmp = new int[len];
while(gap < len) {
for(int i = 0; i < len; i += (2*gap)) {
int left = i;
int right = i + gap;
if(right > len)
right = len;
int mid = left + ((right - left) >> 1);
mergeData(arr, left, mid, right, tmp);
copyArray(tmp, arr, left, right);
Util.printArr(tmp);
}
gap *= 2;
}
}
计数排序:
适合排已知数据范围的(min, max)
将数据放到对应的桶中,桶和元素的值是一一对应的,所以只要遍历数组中存的元素个数就可以知道对应的序列
11 15 12 13 21 25
len = max - min + 1= 15
11 12 13 15 21 25
对应的桶号为0 1 2 4 10 14
排序后序列:11 12 13 15 21 25
public class CountSort{
/**
* 计数排序
* @param arr
*/
public static void countSort(int[] arr) {
int len = arr.length;
int maxValue = arr[0];
int minValue = arr[0];
// 找到区间范围
for(int i = 1; i < len; ++i) {
if(arr[i] > maxValue) {
maxValue = arr[i];
}
if(arr[i] < minValue) {
minValue = arr[i];
}
}
// 申请计数空间,计数空间大小
int proxyLen = maxValue - minValue;
int[] proxy = new int[proxyLen + 1];
// 计数
for(int i = 0; i < len; ++i) {
proxy[arr[i] - minValue]++;
}
// 拷贝回原数组,拷贝次数为原数组元素个数次
for(int j = 0, i = 0; j < len; ++i) {
int count = proxy[i];
while(count-- > 0) {
arr[j++] = i + minValue;
}
}
}
}
基数排序:
原理:主要是对当前位数相同的时候的处理,比如52, 53 54这三个元素在第一次处理个位之后,第二次处理10位的时候顺序就不变了,同理百位。
方法1:LSD,从低位开始排
方法2:LMD,从高位开始排
LSD的步骤:
1. 获取最大数据的位数。
2. LSD: 从低位开始排,个位 -> 十位 -> 百位
3. 每次排好之后就将结果拷贝回原数组
/**
* 基数排序
* @author Z7M-SL7D2
*/
public class RadixSort {
private RadixSort() {}
/**
* 基数排序,LSD,从低位向高位排
* @param arr
*/
public static void radixSort(int[] arr){
int len = arr.length;
if(len <2)
return;
// 求出位数
int digit = getDigit(arr);
// 用于存放出现的次数
int[] proxy = new int[10];
// 用于存放起始地址
int[] startAddr = new int[10];
// 临时存放排好序的数组
int[] tmp = new int[len];
// 倍数
int times = 1;
for(int k= 0; k < digit; ++k) {
// 先求出每个桶里面有多少个元素
for(int i = 0; i < len; ++i) {
proxy[(arr[i] / times) % 10]++;
}
// 求出每个元素在数组中对应的起始地址
int sum = 0;
for(int i = 1; i < 10; ++i) {
sum += proxy[i - 1];
startAddr[i] = sum;
}
// 给临时数组赋值
for(int i = 0; i < len; ++i) {
int pos = ((arr[i] / times) % 10);
tmp[startAddr[pos]++] = arr[i];
}
// 将此次排好序的数组拷贝回原数组。
Util.copyArr(tmp, arr);
// 将地址数组清空
Util.clearArr(startAddr);
// 存个数的数组清空
Util.clearArr(proxy);
Util.printArr(tmp);
times *= 10;
}
}
/**
* 用来获取数组最大位数的函数
* @param arr
* @return
*/
public static int getDigit(int[] arr) {
int len = arr.length;
int count = 1;
int times = 10;
for(int i = 0; i times) {
times *= 10;
++count;
}
}
return count;
}
}
基数排序每次排完之后的结果: