文章目录
直接插入排序
基本思想:我们平时玩扑克牌时,摸牌阶段的排序就用到了插入排序的思想
1、当插入第n个元素时,前面的n-1个数已经有序
2、用这第n个数与前面的n-1个数比较,找到要插入的位置,将其插入(原来位置上的数不会被覆盖,因为提前保存了)
3、原来位置上的数据,依次后移
/**
* 直接插入排序
*/
public void insertSort(int[] array) {
for(int i = 1; i < array.length; i++) {
int j = i-1;
int temp = array[i];
while(j >= 0) {
if(array[j] > temp) {
array[j+1] = array[j];
j--;
}else {
break;
}
}
array[j+1] = temp;
}
}
直接插入排序总结:
①元素越接近有序,直接插入排序的效率越高
②时间复杂度:O(N^2)
最坏的情况下,每次插入一个数字,前面的数字都要挪动一下,一共需要挪动1+2+3+……+n=n(n+1)/2
③空间复杂度:O(1)
没有借助额外的空间,只用到常数个变量
希尔排序
基本思想:
1、先选定个小于n的数字作为gap,所有距离为gap的数分为一组进行预排序(直接插入排序)
2、再选一个小于gap的数,重复①的操作
3、当gap=1时,相当于整个数组就是一组,再进行一次插入排序即可整体有序
具体实现:
多组并排
多次预排序(gap>1)+ 一次插入排序(gap==1)
(1)gap越大,预排越快,越不接近于有序
(2)gap越小,预排越慢,越接近有序
/**
* 希尔排序
* 假设有10个元素,刚开始分为5组,一组2个元素,每个元素之间相隔5个元素
* 假设有n个元素,刚开始分为gap组,一组 n/gap 个元素,每个元素之间相隔 gap 个元素
*/
public void shellSort(int[] array) {
int gap = array.length;
while(gap > 1) {
//这两句的先后顺序比较重要,影响着gap == 1时会不会进行shell排序
gap = gap / 2;
//gap一定会等于1的
shell(array,gap);
}
}
public void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i-gap;
for(; j >= 0;j -= gap) {
if(array[j] > temp) {
array[j+gap] = array[j];
}else{
break;
}
}
array[j+gap] = temp;
}
}
希尔排序总结:
①希尔排序是对直接插入排序的优化
②时间复杂度:O(N^1.3)
③空间复杂度:O(1)
选择排序
基本思想:
每次从数组中选出最大的或者最小的,存放在数组的最右边或者最左边,直到全部有序
形式一:
时间复杂度是:O(n^2)
不稳定
public void slectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int j = i+1;
int min = i;
for(; j < array.length; j++) {
if(array[j] < array[min]) {
min = j;
}
}
if(min != i) {
int temp = array[min];
array[min] = array[i];
array[i] = temp;
}
}
}
形式二:
时间复杂度是:O(n^2)
不稳定
/**
* 拓展
* 选择排序的第二种做法
*/
public void slectSort2(int[] array) {
int left = 0;
int right = array.length - 1;
while(left < right) {
int min = left;
int max = left;
//找最大的与最小的
for (int j = left+1; j <= right; j++) {
//如果一个数即使最大的又是最小的,那么要么就是这组数都是一样的,要么就剩一个数了
if(array[j] > array[max]) {
max = j;
}
if(array[j] < array[min]) {
min = j;
}
}
swap(array,min,left);
//[200,0,8,6,4,1,10]
//max的下标是left; min的下标是1
//将min的值与left交换,此时array[left] = 0 ; array[min] = 200;
//再将max的的值与right交换,此时array[max] = 0;并不是200; 所以会出错
//应该将max = min
if(max == left) {
max = min;
}
swap(array,max,right);
left++;
right--;
}
}
//交换函数
public void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
总结:
对于数组来说找下表比找数值要好
堆排序
基本思想:
1、将待排序的序列构造成一个大堆,根据大堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;
2、将堆顶元素和最后一个元素交换,然后将剩下的节点重新构造成一个大堆;
3、重复步骤2,如此反复,从第一次构建大堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大堆的尾部。最后,就得到一个有序的序列了。
小结论:
排升序,建大堆
排降序,建小堆
时间复杂度:O(n*logn)
空间复杂度O(1)
不稳定
public void heapSort(int[] array) {
//建一个大根堆
createHeap(array);
//交换
int end = array.length-1;
while(end > 0) {
swap(array,0,end);
shiftDown(array,0,end);
end--;
}
}
public static void createHeap(int[] array) {
int end = array.length;
int child = array.length-1;
int parent = (child-1)/2;
while(parent >= 0) {
shiftDown(array,parent,end);
parent--;
}
}
public static void shiftDown(int[]array,int parent,int end) {
int child = parent*2+1;
while(child < end) {
if(child+1 < end && array[child+1] > array[child]) {
child = child+1;
}
if(array[child] > array[parent]){
swap(array,child,parent);
}else {
break;
}
parent = child;
child = parent*2+1;
}
}
public static void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
冒泡排序
冒泡排序的基本思想:
一趟过程中,前后两个数依次比较,将较大的数字往后推,下一次只需要比较剩下的n-1个数,如此往复
时间复杂度O(n^2)
稳定的
public void bubbleSort(int[] array) {
//最外层是趟数
for(int i = 0; i < array.length - 1; i++) {
boolean flg = false;
for(int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]) {
flg = true;
swap(array,j,j+1);
}
}
if(flg == false) {
break;
}
}
}
快速排序
Hoare
时间复杂度:n*log n (理想状态下,接近一个满二叉树)
空间复杂度:logn (左子树完成之后 回收空间 再完成右子树)
不稳定
当我们给的数据是有序的时候,时间复杂度是n^2 , 空间复杂度是 n
思路:
找基准,基准的左边一定要与基准,基准的右边一定大于基准。
二分
递归
hoare的单趟思想:
1、左边作key,右边先走找到比key小的值
2、左边后走找到大于key的值
3、然后交换left和right的值
4、一直循环重复上述1 2 3步
5、两者相遇时的位置,与最左边选定的key值交换
这样就让key到达了正确的位置上
public void quick(int[] array) {
int begI = 0;
int endI = array.length-1;
QuickSort(array,begI,endI);
}
public void QuickSort(int[] array, int begI, int endI) {
//能不能不写>号,预防 1 2 3 4 5, 直接没有左树,或右树,
if(begI >= endI) {
return;
}
//找基准小标,基准左边的一定小于等于基准,基准右边的一定大于等于基准
int benchmark = partition(array,begI,endI);
QuickSort(array,begI,benchmark-1);//对基准左边的数据进行排序
QuickSort(array,benchmark+1,endI);//对基准右边的数据进行排序
}
//找基准下标
public int partition(int[] array,int left,int right) {
int temp = array[left]; //把左边的作为基准
int index = left; //记录一下数组的起始位置
while(left < right) {
//left < right &&这个条件不能少,预防后面的right都大于left,数组越界
while (left < right && array[right] >= temp) {
//注意这里的等于号,当array[right] == temp,不要交换,否则会陷入死循环。
right--;
}
//right下标的值小于temp
while(left < right && array[left] <= temp) {
left++;
}
//left下标的值大于temp
//交换
swap(array,left,right);
}
//交换和原来的left
swap(array,index,left);
return left;
}
时间复杂度分析
时间复杂度:n*log n
partition函数的时间复杂度是 n ,因为里面的while循环并不是嵌套的。
递归的第一层:执行partition函数 1 次,时间复杂度为 n
递归的第二层:执行partition函数 2 次,时间复杂度为 n
递归的第三层:执行partition函数 4 次,时间复杂度为n
递归的第四层:执行partition函数 8 次,时间复杂度为n
递归的第 log n 层:执行partition函数 2^(logn-1)次,时间复杂度为n
总的时间复杂度是: log n *n 每一层时间复杂度是n,一共logn层
注意点:
(1)
内置的while(left < right && array[right] >= pivot) 需要注意的是这个大于等于号改写成大于号可不可以。
答案是不可以,因为当【(left下标所指的值)(right下标所指的值) (pivot的值)】时,会陷入死循环。
(2)
当把数组最左侧的值当作基准值时,要先从右边开始进行比较。否则会把大于基准值的数放在基准的左侧。
当把数组最右侧的值当作基准值时,要先从左边开始进行比较。
快排优化
挖坑法
挖坑法单趟思想:
1、先将最左边第一个数据存放在临时变量key中,形成一个坑位
2、右边先出发找到小于key的值,然后将该值丢到坑中去,此时形成一个新坑位
3、左边后出发找到大于key的值,将该值丢入坑中去,此时又形成一个新的坑位
4、一直循环重复1 2 3步
5、直到两边相遇时,形成一个新的坑,最后将key值丢进去
这样key就到达了正确的位置上了
/**
* 快速排序的优化
* 挖坑发
*/
public void quick(int[] array) {
int begI = 0;
int endI = array.length-1;
QuickSort(array,begI,endI);
}
public void QuickSort(int[] array, int begI, int endI) {
if(begI >= endI) {
return;
}
int benchmark = partition(array,begI,endI);
QuickSort(array,begI,benchmark-1);
QuickSort(array,benchmark+1,endI);
}
public int partition(int[] arr, int left,int right) {
int pivot = arr[left];//起始坑位
while(left < right) {
while(left < right && arr[right] >= pivot) {
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= pivot) {
left++;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
前后指针法
前后指针的思想:
1、初始时选定prev为序列的开始,cur指针指向prev的后一个位置,同样选择最左边的第一个数字作为key
2、cur先走,找到小于key的值,找到就停下来
3、++prev
4、交换prev和cur为下标的值
5、一直循环重复2 3 4步,停下来后,最后交换key和prev为下标的值
这样key同样到达了正确的位置
public int partition2(int[] arr, int left,int right) {
int privot = arr[left];//基准
int prev = left;
int cur = left+1;
for(; cur <= right; cur++) {
if(arr[cur] < privot && ++prev != cur) {
/**
++prev != cur 防止cur和prev相等时,相当于自己和自己交换,可以省略
同时注意:++prev这个地方非常的巧妙,因为这么个原理,cur只要找到小的,prev就要向前走,不管cur与prev的位置到底重不 重合,比如说cur找到了5个小的,那么prev就应该放5个小的,然后再最后再与privot进行交换。所以说cur只要找到小的prev 就应该++。
*/
/**
prev这个地方非常的巧妙,当cur找到小的时候,prev才会加加,然后交换。cur找到小的之前,prev一直都指向小的或基准位 置。所以最后cur遍历完了之后,prev可以直接与基准进行交换。
prev从left进行开始的优点是 【6 7 1 2 5 3 9 8】当第二个元素大与基准时可以进行移动。如果prev与cur都从left+1开 始,那么当第二个元素大于基准时,是跳过的。
*/
//交换
swap(arr, cur, prev);
}
}
swap(arr, prev, left);
return prev;
}
public static void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
三数取中法
快速排序对于数据是敏感的,如果这个序列是非常无序,杂乱无章的,那么快速排序的效率是非常高的,可是如果数列有序,时间复杂度就会从O(N*logN)变为O(N^2),相当于冒泡排序了
若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN)
但是这是理想情况,当我们面对一组极端情况下的序列,就是有序的数组,选择左边作为key值的话,那么就会退化为O(N^2)的复杂度,所以此时我们选择首位置,尾位置,中间位置的数分别作为三数,选出中间位置的数,放到最左边,这样选key还是从左边开始,这样优化后,全部都变成了理想情况
public void quickS(int[] array) {
QuickSorts(array,0,array.length-1);
}
public void QuickSorts(int[] array, int begI, int endI) {
if(begI >= endI) {
return;
}
/**
* 最坏的情况是排列一组有序的数据,时间复杂度是n^2,空间复杂度是n,
* 我们采用三数取中法进行划分,这样可以使数据分布的更均匀。
* 【1,2,3,4,5,6,7】--> 【4,2,3,1,5,6,7】--> 【1,2,3,4,5,6,7】
* 初始数组 优化后数组 找到基准是4,而不是1
*/
int mid = findMidIndex(array,begI,endI);
swap(array,mid,begI);
int benchmark = partition4(array,begI,endI); //找基准
QuickSort(array,begI,benchmark-1);
QuickSort(array,benchmark+1,endI);
}
//寻找大小介于中位的下标
public int findMidIndex(int[] array,int begI,int endI) {
int mid = (begI+endI)/2;
if(array[endI] > array[begI]) {
if(array[mid] < array[begI]) {
return begI;
}else if(array[mid] > array[endI]) {
return endI;
}else{
return mid;
}
}else{
if(array[mid] > array[endI]) {
return endI;
}else if(array[mid] < array[begI]) {
return begI;
}else {
return mid;
}
}
}
public int partition4(int[] arr, int left,int right) {
int cur = left+1;
int prev = left;//prev必须从left开始,避免当第二个元素大于基准时,跳过。
int priot = arr[left];//基准
while(cur <= right) {
if(arr[cur] < priot && prev != cur) {
prev++;
swap(arr,prev,cur);
}
cur++;
}
swap(arr,left,prev);
return prev;
}
递归到小区间
随着递归深度的增加,递归次数以每层2倍的速度增加,这对效率有着很大的影响,当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
我们可以当划分区间长度小于10的时候,用插入排序对剩下的数进行排序
public void quick(int[] array) {
QuickSort(array,0,array.length-1);
}
public void QuickSort(int[] array, int begI, int endI) {
if(begI >= endI) {
return;
}
if(endI-begI+1 <= 10) {
insertSort(array,begI,endI);
return;
}
int mid = findMidIndex(array,begI,endI);
swap(array,mid,begI);
int benchmark = partition4(array,begI,endI); //找基准
QuickSort(array,begI,benchmark-1);
QuickSort(array,benchmark+1,endI);
}
public void insertSort(int[] array, int left, int right) {
for(int i = left+1; i<= right; i++) {
int temp = array[i];
int j = i-1;
while(j >= left) {
if(array[j] > temp) {
array[j+1] = array[j];
}else { // 出现if就想想有没有else
break;
}
j--;
}
array[j+1] = temp;
}
}
public int findMidIndex(int[] array,int begI,int endI) {
int mid = (begI+endI)/2;
if(array[endI] > array[begI]) {
if(array[mid] < array[begI]) {
return begI;
}else if(array[mid] > array[endI]) {
return endI;
}else{
return mid;
}
}else{
if(array[mid] > array[endI]) {
return endI;
}else if(array[mid] < array[begI]) {
return begI;
}else {
return mid;
}
}
}
public int partition4(int[] arr, int left,int right) {
int cur = left+1;
int prev = left;
int priot = arr[left];//基准
while(cur <= right) {
if(arr[cur] < priot && prev != cur) {
if(arr[prev] > priot) {
swap(arr,prev,cur);
}else {
prev++;
swap(arr,prev,cur);
}
}
cur++;
}
swap(arr,left,prev);
return prev;
}
public static void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
递归版本的优化完整版
public void quick(int[] array) {
QuickSort(array,0,array.length-1);
}
public void QuickSort(int[] array, int begI, int endI) {
if(begI >= endI) {
return;
}
if(endI-begI+1 <= 10) {
insertSort(array,begI,endI);
return;
}
int mid = findMidIndex(array,begI,endI);
swap(array,mid,begI);
int benchmark = partition4(array,begI,endI); //找基准
QuickSort(array,begI,benchmark-1);
QuickSort(array,benchmark+1,endI);
}
public void insertSort(int[] array, int left, int right) {
for(int i = left+1; i<= right; i++) {
int temp = array[i];
int j = i-1;
while(j >= left) {
if(array[j] > temp) {
array[j+1] = array[j];
}else { // 出现if就想想有没有else
break;
}
j--;
}
array[j+1] = temp;
}
}
public int findMidIndex(int[] array,int begI,int endI) {
int mid = (begI+endI)/2;
if(array[endI] > array[begI]) {
if(array[mid] < array[begI]) {
return begI;
}else if(array[mid] > array[endI]) {
return endI;
}else{
return mid;
}
}else{
if(array[mid] > array[endI]) {
return endI;
}else if(array[mid] < array[begI]) {
return begI;
}else {
return mid;
}
}
}
public int partition4(int[] arr, int left,int right) {
int cur = left+1;
int prev = left;
int priot = arr[left];//基准
while(cur <= right) {
if(arr[cur] < priot && prev != cur) {
if(arr[prev] > priot) {
swap(arr,prev,cur);
}else {
prev++;
swap(arr,prev,cur);
}
}
cur++;
}
swap(arr,left,prev);
return prev;
}
public static void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
非递归版本
递归的算法主要是在划分子区间,如果要非递归实现快排,只要使用一个栈来保存区间就可以了。一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。
非递归的基本思想:
申请一个栈,存放排序数组的起始位置和终点位置。
将整个数组的起始位置和终点位置入栈。
由于栈的特性是:后进先出,right后进栈,所以right先出栈。
定义一个end接收栈顶元素,出栈操作、定义一个begin接收栈顶元素,出栈操作。
对数组进行一次单趟排序,返回key关键值的下标。
这时候需要排基准值key左边的序列。
如果只将基准值key左边序列的起始位置和终点位置存入栈中,等左边排序完将找不到后边的区间。所以先将右边序列的起始位 置和终点位置存入栈中,再将左边的起始位置和终点位置后存入栈中。
6.判断栈是否为空,若不为空 重复4、5步、若为空则排序完成。
/**
* 快速排序的非递归实现
*/
public void quick(int[] array) {
quickSort(array,0,array.length-1);
}
public void quickSort(int[] array, int begI,int endI) {
Stack<Integer> stack = new Stack<>();
int piovt = partition4(array,begI,endI);//找到基准下标
// 1: 判断右面有没有多于两个元素
//这个条件必须要有,否则就死循环(1个元素)或者越界(0个元素)。
if(endI-1 > piovt) {
stack.push(piovt+1);
stack.push(endI);
}
//判断左边有没有多于两个元素
if(begI+1 < piovt) {
stack.push(begI);
stack.push(piovt-1);
}
while(!stack.isEmpty()) {
endI = stack.pop();
begI = stack.pop();
piovt = partition4(array,begI,endI);//找到基准下标
if(endI-1 > piovt) {
stack.push(piovt+1);
stack.push(endI);
}
if(begI+1 < piovt) {
stack.push(begI);
stack.push(piovt-1);
}
}
}
public int partition4(int[] arr, int left,int right) {
int cur = left+1;
int prev = left;
int priot = arr[left];//基准
while(cur <= right) {
if(arr[cur] < priot && prev != cur) {
if(arr[prev] > priot) {
swap(arr,prev,cur);
}else {
prev++;
swap(arr,prev,cur);
}
}
cur++;
}
swap(arr,left,prev);
return prev;
}
public static void swap(int[] array,int i,int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
归并排序
时间复杂度:O(N*logN)
空间复杂度:O(N) 因为你要在合并函数的时候要开辟一个新的数组,这个数组最坏情况下长度是N。
稳定性:稳定
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
递归实现
归并排序的基本思想(分治思想):
1、(拆分)将一段数组分为左序列和右序列,再将左序列细分为左序列和右序列,如此重复该步骤,直到细分到区间不存在或者只有一个数字为止
2、(合并)将第一步得到的数字合并成有序区间
从思想上来说和二叉树很相似,所以我们可以用递归的方法来实现归并排序
public void mergeSort(int[] array) {
mergeS(array,0,array.length-1);
}
public void mergeS(int[] array, int left, int right) {
if(left >= right) {
return;
}
int mid = (left+right)/2;
mergeS(array,left,mid);
mergeS(array,mid+1,right);
//合并
combine(array,left,right,mid);
}
//合并函数
public void combine(int[] array,int left ,int right, int mid) {
//将两个有序数组合并为一个有序数组
int sl = left;
int el = mid;
int sr = mid+1;
int er = right;
int[] arr = new int[right-left+1];
int k =0;
while(sl <= el && sr <= er) {
if(array[sl] < array[sr]) {
arr[k] = array[sl];
k++;
sl++;
}else{
arr[k] = array[sr];
k++;
sr++;
}
}
while(sl <= el) {
arr[k] = array[sl];
sl++;
k++;
}
while(sr <= er) {
arr[k] = array[sr];
sr++;
k++;
}
for(int j = 0; j < k; j++) {
array[j+left] = arr[j];
/**
注意:arr数组中的下标是从0-k的,而array数组的下标是从left到right的,
所以赋值的时候用array[j+left] = arr[j];
*/
array[j+left] = arr[j];
}
}
非递归实现
我们知道,递归实现的缺点就是会一直调用栈,而栈内存往往是很小的。所以,我们尝试着用循环的办法去实现
由于我们操纵的是数组的下标,所以我们需要借助数组,来帮我们存储上面递归得到的数组下标,和递归的区别就是,递归要将区间一直细分,要将左区间一直递归划分完了,再递归划分右区间,而借助数组的非递归是一次性就将数据处理完毕,并且每次都将下标拷贝回原数组
归并排序的基本思路是将待排序序列a[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。
但是我们这是理想情况下(偶数个),还有特殊的边界控制,当数据个数不是偶数个时,我们所分的gap组,势必会有越界的地方
第一种情况:
第二种情况:
public void mergeSor(int[] array) {
int gap = 1;
while(gap < array.length) {
for(int i = 0; i< array.length; i = i+gap*2) {
/**
这里的i就是left,进行一堆一堆的合并
left是不会越界的。但是mid与right可能会存在越界
*/
int left = i;
int mid = left+gap-1;
int right = mid + gap;
//如果mid越界就调整一下
if(mid >= array.length) {
mid = array.length-1;
}
//如果right越界就调整一下
if(right >= array.length) {
right = array.length-1;
}
combine(array,left,right,mid);
}
gap = gap*2;
}
}
public void combine(int[] array,int left ,int right, int mid) {
//将两个有序数组合并为一个有序数组
int sl = left;
int el = mid;
int sr = mid+1;
int er = right;
int[] arr = new int[right-left+1];
int k =0;
while(sl <= el && sr <= er) {
if(array[sl] < array[sr]) {
arr[k] = array[sl];
k++;
sl++;
}else{
arr[k] = array[sr];
k++;
sr++;
}
}
while(sl <= el) {
arr[k] = array[sl];
sl++;
k++;
}
while(sr <= er) {
arr[k] = array[sr];
sr++;
k++;
}
for(int j = 0; j < k; j++) {
array[j+left] = arr[j];
/**
注意:arr数组中的下标是从0-k的,而array数组的下标是从left到right的,
所以赋值的时候用array[j+left] = arr[j];
*/
array[j+left] = arr[j];
}
}
海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
计数排序
又叫非比较排序,又称为鸽巢原理,是对哈希直接定址法的变形应用
①统计相同元素出现的个数
对于给定的任意数组a,我们需要开辟一个计数数组count,a[i]是几,就对count数组下标是几++
这里我们用到了绝对映射,即a[i]中的数组元素是几,我们就在count数组下标是几的位置++,但是对于数据比较聚集,不是从较小的数字开始,例如1001,1002,1003,1004这样的数据,我们就可以用到相对映射的方法,以免开辟数组空间的浪费,count数组的空间大小我们可以用a数组中最大值减去最小值+1来确定(即:range=max-min+1),我们可以得到count数组下标 j =a[i]-min
②根据count数组的结果,将数据拷贝回a数组
count[j]中数据是几,说明该数出现了几次,是0就不用拷贝
public void JiShuSort(int[] array) {
//1:遍历数组找到最大值与最小值
int maxVal = array[0];
int minVal = array[0];
for (int i = 0; i < array.length; i++) {
if(array[i] > maxVal) {
maxVal = array[i];
}
if(array[i] < minVal) {
minVal = array[i];
}
}
//2:确定数组的长度
int len = maxVal-minVal+1;
/**
确定数组的长度:len = 最大值-最小值 + 1 :最坏的情况是介于最大值与最小值之间不同的数都有,我们要分别对其进行计数
这个len代表着种类的个数。
*/
int[] arrayCount = new int[len];
//3: 计数
for (int i = 0; i < array.length; i++) {
int index = array[i] - minVal;//相对映射
arrayCount[index]++;
}
//4: 排序
int size = 0;
for(int i = 0; i < len; i++) {
while(arrayCount[i] != 0) {
array[size] = i + minVal;
size++;
arrayCount[i]--;
}
}
}
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
八大排序的稳定性总结:
稳定的排序有:直接插入排序,冒泡排序、归并排序 计数排序
不稳定的排序有:希尔排序、选择排序、堆排序、快速排序、
部分图片和文字参考 " Hero 2021 " .