一 排序
文章目录
参考:十大经典排序算法
排序算法的时间复杂的和空间复杂度:
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
插入排序 | O(n) | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n1.3) | O(n2) | O(1) | 不稳定 |
堆排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(1) | 不稳定 |
快速排序 | O(n*log(n)) | O(n*log(n)) | O(n2) | O(log(n)) ~O(n) | 不稳定 |
归并排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(n) | 稳定 |
稳定:若a=b,且a在b的前面,经过排序后a仍在b的前面,则称该排序算法稳定。
稳定的排序:插入排序,冒泡排序,归并排序
不稳定的排序:堆排序,快速排序,选择排序,希尔排序
比较排序:数据之间的排序依赖它们之间的比较。
非比较排序:根据数据在空间中的位置进行排序,对数据规模和数据分布有一定要求。
- 一个稳定的排序,可以实现为不稳定的排序(更改一个等号的问题)
- 一个不稳定的排序,不可能实现为稳定的排序
1 直接插入排序(稳定)
拿升序举例:拿到无序数组的第一个元素ch,来和前面有序数组的元素进行比较,比ch元素大的元素向后移动,当遇到小于等于ch的元素时直接break(因为前面的元素只会更小),进而找到一个合适的位置进行插入。
static void insertSort(int[] array){
//假定第一个元素有序了,从第二个元素开始和前面有序的数据
//进行比较,找位置插入
for (int i =1 ; i <array.length ; i++) {
int ch=array[i];
//这整个循环是一个挪位的过程
int j=0;
for (j = i-1; j >=0; j--) {
if(ch<array[j]){
array[j+1]=array[j];
}else{
break;
}
}
//插入
array[j+1]=ch;
}
}
时间复杂度:
最佳(数据有序时):O(n)
最坏(数据逆序时):O(n2)
平均:O(n2)
空间复杂度: O(1)
注意:
数据越有序,插入排序的时间效率越高。
2 希尔排序(不稳定)
希尔(shell)排序,是插入排序的一种优化,主要是根据“序列越有序,插入排序的时间效率越高”这一原则改进。即通过将数据进行分组插入排序,分组内元素距离的大小称为增量,希尔增量(希尔增量不是最优的增量)的大小可通过:gap1=length/2; gap2=gap1/2;…;gapN=1.进行确定。最后一个增量必须为1.
代码形式上和插入排序相同。
static void shellSort(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
int ch = array[i];
int j=0;
for (j = i-gap; j >=0 ; j-=gap) {
if(ch<array[j]){
array[j+gap]=array[j];
}else{
break;
}
}
//插入
array[j+gap]=ch;
}
}
public static void main(String[] args) {
int[] a={1,6,3,4,9,4,3,2,8};
int[] gap={4,2,1};
for (int i = 0; i < gap.length; i++) {
shellSort(a,i);
}
System.out.println(Arrays.toString(a));
}
时间复杂度:
O(n^1.3-1.5)
空间复杂度:
O(1)
3 选择排序(不稳定)
依次从后面的无序区间中取值,和前面有序区间的最后一个元素进行比较,若无序区间的值小,则进行交换,否则则保持不变(不用交换,当前位置就是最小值),然后再继续从无序区间中取值比较。
代码连续两个循环搞定:
public static void selectSort(int[] array){
int i;
for (i=0; i <array.length-1 ; i++) {
int min=i;
//每次循环都到无序队列里面找一个最小的
for (int j = i+1; j <array.length ; j++) {
if(array[j]<array[min]){
min=j;
}
}
if(min!=i){
int tmp = array[min];
array[min] = array[i];
array[i] = tmp;
}
}
}
不稳定性理解:
举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
时间复杂度:
最佳:O(n2)
最坏:O(n2)
平均:O(n2)
空间复杂度:
O(1)
4 堆排序(不稳定)
前置知识点了解:完全二叉树中父节点和子节点的关系:
leftChild = 2*parent+1;
rightChild = 2*parent +2;
parent = (child-1)/2
升序排列数组要建大堆
降序排列数组要建小堆
本例中我们采用升序排序,建一个大堆。
(array.length-1-1)/2:
获取最后一个子节点的父节点
创建一个大堆:
将这个数组array看作是完全二叉树层序遍历的结果
每颗子树都要调整为大堆
首先要找到最后一个子节点的父节点(array.length-1-1)/2
//1 创建一个大堆
public void createHeap(int[] array){
//找到最后一个节点的父节点,依次向下调整
for(int i=(array.length-1-1)/2;i>=0;i--){
adjustDown(array,i,array.length);
}
}
向下调整:
public void adjustDown(int[] array,int root,int len){
//时间复杂度log2n
int parent = root;
int child = 2*parent+1;
//看看是否有孩子节点
//每次循环都是对一棵树的调整
while(child<len){
//是否有右孩子节点
if(child+1<len&&array[child]<array[child+1]){
child++;
}
//child保存的是最大值的下标
if(array[child]>array[parent]){
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
//向下调整,即让parent指向child
parent = child;
child=2*parent+1;
}else{
break;
}
}
}
接下来简单描述一下为啥要向下调整:
以建一个大堆为例:
数组:0 9 1 5 7 2 3
第一次调整,让parent指向最后一个子节点的父节点,进行第一次调整。
对于已经是大堆的子树,直接break掉了。
当出现这种情况时,就能够清楚向下调整的意义所在了。
通过向下调整,被改变的子树,重新恢复为大堆,整个大堆建造过程结束。
创建完堆之后进行堆排序,因为是大堆,头节点保存的是最大的元素。堆排序就是将堆顶元素不断向后面的有序数组前添加,这就有点类似于选择排序。
/2 堆排序(默认为大堆),从小到大,第一个和最后一个交换
public void heapSort(int[] array){
//传入一个数组,先建立一个大堆
createHeap(array);
int end = array.length-1;
while(end>0){
//将堆顶元素和end元素进行交换
int tmp = array[end];
array[end] = array[0];
array[0] = tmp;
//end=array.length-1,此处的end代表数组长度
//交换之后,通过向下调整,重新建立一个大根堆
adjustDown(array,0,end);
end--;
}
}
注意:
在向下调整时,传的是数组的大小adjustDown(array,0,end);
,而堆排序里面的end指的是数组的下标,所以要在 end--;
之前进行向下调整adjustDown(array,0,end);
。
时间复杂度:
最佳:O(nlog2n)
最坏:O(nlog2n)
平均:O(nlog2n)
建堆的时间复杂度为:O(nlog2n)
空间复杂度:
堆排序是就地排序所以空间复杂度是常数,O(1)
5 冒泡排序(稳定)
通过相邻数的比较将最大的数冒泡到无序区间的最后。冒泡排序是一个从下向上,从0到array.length-1-i
,依次比较的过程。
public static void bubbleSort(int[] array){
//外层for循环控制冒泡的次数,每一次冒泡的完成代表,
//完成一个数字有序排序
for (int i = 0; i <array.length-1 ; i++) {
//内层for循环控制每次冒泡进行交换的次数
//如果在一次冒泡的过程中,一次都没有进行交换,则说明
//该数组已有序,退出冒泡排序即可
boolean flg = false;
for (int j = 0; j <array.length-1-i ; j++) {
if(array[j]>array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if(!flg){
break;
}
}
}
时间复杂度:
数据有序时 O(n)
数据逆序时 O(n^2)
空间复杂度:
O(1)
6 快速排序(不稳定)
- 从待排序区间以某种方式选择一个数,作为基准值(pivot)
- partition(核心):遍历整个待排序区间,将比基准值小的放到基准值的左边,比基准值大的放到基准值的右边
- 采用分治思想,对分好的左右区间以同样的方式进行递归处理,直到小区间长度==1.
(1)快速排序的递归形式
基准值: 默认的都是以第一个下标low对应的元素作为基准值
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
public void quick(int[] array,int low, int high){
if(low>=high){
return;
}
//依据递归的思想,每次都是找基准点,划分区间
int pivot = partion(array,low,high);
quick(array,low,pivot-1);
//如果pivot为最后一个元素,此时pivot+1将会有low大于high的情况
quick(array,pivot+1,high);
}
public int partion(int[] array,int low,int high){
int tmp =array[low];//默认的基准值
while(low<high){
//右边找小于tmp的元素,赋给array[low]
while((low<high)&&array[high]>=tmp){
high--;
}
if(low>=high){
break;
}else{
array[low] = array[high];
}
//左边找大于tmp的元素,赋给array[high]
while((low<high)&&array[low]<=tmp){
low++;
}
if(low>=high){
break;
}else{
array[high] = array[low];
}
}
array[low] = tmp;
return low;
}
时间复杂度:
理想情况下: O(nlog2n)
数据有序: O(n^2)
-
快速排序的一次划分算法从两头交替搜索,直到low和hight重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
-
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过
log2n
趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。 -
最坏的情况是,每次所选的基准数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)
空间复杂度:
O(log2n)
(2)快速排序的非递归形式:
用栈记录区间的端点low和high,
// 快速排序的非递归形式
public void quickSort(int[] array) {
quick(array,0,array.length-1);
}
public void quick(int[] array,int low,int high) {
int pivot = partion(array,low,high);
Stack<Integer> stack = new Stack<>();
//>low+1保证左边有两个元素以上,并将左区间入栈
if(pivot > low+1 ) {
stack.push(low);
stack.push(pivot-1);
}
//<high-1保证右边有两个元素以上,并将右区间入栈,
//假如右边只有一个元素了,根据pivot的右边都比pivot大,
//所以没必要再对右区间进行排序。
if(pivot < high-1) {
stack.push(pivot+1);
stack.push(high);
}
while (!stack.empty()) {
high = stack.pop();
low = stack.pop();
pivot = partion(array,low,high);
if(pivot > low+1 ) {
stack.push(low);
stack.push(pivot-1);
}
if(pivot < high-1) {
stack.push(pivot+1);
stack.push(high);
}
}
}
使用栈来做,因为是先进后出,所以一般是先对一半区间进行快排,然后再对另一半区间进行快排。
(3)快速排序的优化:
- 设定区间阈值,当待排序区间长度小于该阈值时,采用直接插入排序对剩下的区间进行排序(因为对于插入排序,数据越有序,排序效率越高)。
- 对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。固定位置(默认),三数取中法,随机选取基准值。
1)设定区间阈值:
基于直接插入排序的 ”区间越有序,直接插入排序性能越高“ 原则,在快速排序经过多次partition调整之后,区间已经逐渐趋于有序,此时对小区间进行直接插入排序,可在一定程度上提高快速排序的效率。
// 7.1 快速排序优化1——设置阈值,如果区间长度小于该阈值,则用直接插入排序
//因为,区间越有序,直接插入排序性能越高,为O(n)
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
public void quick(int[] array,int low,int high){
if(low>=high){
return;
}
//区间长度小于该阈值,则用直接插入排序
if(high-low+1<100){
insertSort(array,low,high);
return;
}
int pivot = partion(array,low ,high);
quick(array,low,pivot-1);
quick(array,pivot+1,high);
}
public void insertSort(int[] array,int low ,int high){
for (int i = low+1; i <=high ; i++) {
int tmp = array[i];
int j =0;
for ( j =i-1; j >=low; j--) {
if(array[j]>tmp){
array[j+1]=array[j];
}else{
break;
}
}
array[j+1] = tmp;
}
}
2)三数取中:
构造array[mid] <= array[low] <= array[high]
让中间值最小。
三数取中能够在一定程度上保证区间划分时为“等长区间”,进而提高快速排序的效率。
// 三数取中:在区间已经趋于有序的情况下,也是快排最坏的情况
//时间复杂度为O(n^2),
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
public void quick(int[] array,int low,int high){
if(low>=high){
return;
}
ThreeNumOfMiddle(array,low,high);
int pivot = partion(array,low ,high);
quick(array,low,pivot-1);
quick(array,pivot+1,high);
}
public static void swap(int[] array,int low,int high) {
int tmp = array[low];
array[low] = array[high];
array[high] = tmp;
}
public static void ThreeNumOfMiddle
(int[] array,int low,int high) {
//构造这种关系array[mid] <= array[low] <= array[high];
//让中间值最小
int mid = (low+high)/2;
if(array[mid] > array[high]) {
swap(array,mid,high);
}
if(array[mid] > array[low]) {
swap(array,mid,low);
}
if(array[low] > array[high]) {
swap(array,low,high);
}
}
快速排序的优化参考:https://blog.csdn.net/insistgogo/article/details/7785038
7 归并排序参考:
https://blog.csdn.net/glpghz/article/details/104374968
1)外部排序:
当需要排序的数据特别大,而内存放不下时,需要将数切割成n小份进行排序。归并排序是最常用的外部排序,即通过多路归实现大数据的排序。
8 基于非比较的排序:
(1)桶排序
桶排序的工作原理是将数据分装到有限数量的桶里,对每个桶分别进行排序,如果能将数据均匀分配,排序的速度将是很快的。我们首先需要知道所有待排序元素的范围,然后根据数据范围准备同样数量的桶,接着把元素放到对应的桶中,最后按顺序输出。
一个桶并不总是放同一个元素,在很多时候一个桶里可能会放多个元素。除了对一个桶内的元素做链表存储,我们也有可能对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。
桶排序:https://blog.csdn.net/liupeifeng3514/article/details/83383429
(2)基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。 由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
(3)计数排序
计算排序是非比较的排序算法,在对一定范围内的整数排序时,它有很大优势,这是空间换时间的典型做法。基于范围中最大的数k和长度n,计数排序的时间复杂度为O(k+n)。计数排序使用一个额外的数组,其中第i个元素值是待排序数组中值等于i的元素的个数。计数排序只能对整数进行排序。
计数排序参考链接
(4)位图排序
使用提示,如何想到使用计数排序或者在海量数据处理方面使用计数排序的思想呢?如果我们知道所有的数字只出现一次,我们就可以只使用计算排序中的记录函数,将所有存在的值对应的位置设置为1,否则对应为0,扫描整个数组输出位置为1对应的下标即可完成排序。这种思想可以转为位图排序。
我们使用一个位图来表示所有的数据范围,01位串来表示,如果这个数字出现则对应的位置就是1,否则就是0.例如我们有一个集合S = {1,4,2,3,6,10,7}; 注意到最大值10,用位图表示为1111011001,对应为1的位置表示这个数字存在,否则表示这个数字不存在。1111011001对应1 2 3 4 5 6 7 8 9 10。
参考博客:位图排序
注意:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值(简单版桶排序);
- 桶排序:每个桶存储一定范围的数值;
- 归并排序,分而治之,先排后归。
快速排序,以轴划分,左右并行。
桶排序, 划分进桶,桶中排序。
计数排序,确定范围,填充位置。