- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
排序在很多场合都能用到,并且排序的方法有很多,下面介绍的是七种比较常见的基于比较的排序。
稳定性:能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
注:一个稳定的排序是由代码决定的,稳定的排序可以变为不稳定的排序,而不稳定的排序一定不能变为稳定的排序。
“基于比较”可以有些读者和认为排序不都是基于比较的吗?但是,有个排序方法不用根据大小就能够排序——基数排序。
基数排序它是根据每位数的大小来比较的。首先,创建9个大小相等的队列。再根据每个数的个位数去比较大小。
例如:初始化:
根据个位数去比较大小:
从左到右依次出队。
再根据十位数的大小去入队:
最后依次出列则已经排好序。
基数排序的空间复杂度太大。因此基数排序我们只作了解即可,只不过它比较特殊不需要排序就能够排出大小。
思路:直接插入排序它是从下标为1的数开始,将i下标的元素放入tmp中,若有元素比tmp中的数大,则放在j+1的位置,j再往后回退。目的是将最小的数放在最后排序时的最终位置。
例如:
初始化:令i从下标1开始,j从i-1开始。
将i下标的元素放入tmp中,比较j下标的元素。若j下标的元素比tmp大,则有arr[j+1]=arr[j]
。
此时j小于0,则遍历i。i到2下标,重复相同的操作。但是若有tmp>arr[j]
,直接跳出现在i下标处的循环,i到下一个位置。
最后如果退出这一趟的循环,则记得要将arr[j+1]=tmp
,将tmp的值放入数组当中。
代码:
public static void insertSort(int[] array) {
for(int i = 1;i < array.length;i++) {//n-1
int tmp = array[i];
int j = i-1;
for(; j >= 0;j--) {//n-1
if(array[j] > tmp) {
array[j+1] = array[j];
}else{
break;
}
}
array[j+1] = tmp;
}
}
特点:当一组数据数据量不大且趋近有序,此时用直接插入排序是比较快的,并且是数组越有序越快。
时间复杂度:
空间复杂度:O(1) 。
稳定性:稳定。
若要将直接插入排序的稳定性变为不稳定,则只需将array[j] > tmp
改为array[j] >= tmp
即可。例如一个数组由16*(星号当中标记),16,18组成,则i为16的位置,j为16 * 的位置,因为 16 * 大于等于16,则16* 移到16的位置,最后j- -,j小于0,则16移到了16*的位置。结果为:16,16 * ,18 。
在有序区间选择数据应该插入的位置时,因为区间的有序性,可以利用折半查找的思想。
public static void bsInsertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int v = array[i];
int left = 0;
int right = i;
// [left, right)
// 需要考虑稳定性
while (left < right) {
int m = (left + right) / 2;
if (v >= array[m]) {
left = m + 1;
} else {
right = m;
}
}
// 搬移
for (int j = i; j > left; j--) {
array[j] = array[j - 1];
}
array[left] = v;
}
}
希尔排序是基于直接插入排序的优化。
思想:假设10000个数据,若用直接插入排序则最坏情况下的时间复杂度为1000010000=1亿,若采用分组的思想,10000个数据分为100组,每组100个,则每组的时间复杂度为100100=10000,因为有100则,则100组的时间复杂度为10000*100=100w。远小于直接插入排序的时间复杂度。而希尔排序就是用的分组的思想。
例子:
我们先将一个数组分为5组。则跨度为5,有:
我们原理上可以是先将每组的数字排序。例如12,8,7排序后为7,8,12 。每一组都是相同的排序。
最后有:
但是我们求出来的只是每个组有序。此时我们可以将跨度改为3,即3组。有:
相同地,将每组的数据排序后,有:(虽然跨度越来越小,但是数组本身越来越有序)
最后将跨度变为1,则每一个数据为一组,等同于直接插入排序。
理论上是以上的思想。但是代码上有所不同。
如果代码上直接实现每组排序是比较困难的。我们不妨先将i到每组的中间位置,j为每组的起始位置。
初始化:
因为8比12小,有:
**此时令i++,比较的是第二组中的中间位置与起始位置的数值。**发现5比33小,5回退5个单位,小于0,则再i++。
我们发现当i++一直加到最后一组的中间元素之后,比较的是第一组的最后一个元素与中间元素。
如:(j每次要比i小gap个单位)。
需要注意的是,最后一次的跨度一定要为1,否则无法保证有序。
public static void shellSort(int[] array) {
//处理gap
int gap = array.length;
while (gap > 1) {
gap = gap / 3 + 1;//+1是为了保证最后一个序列是1,其实除几都行
// gap /= 2;
shell(array,gap);
}
}
public static void shell(int[] array,int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i-gap;
for (; j >= 0; j -= gap) {
if(array[j] > tmp) {
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap] = tmp;
}
}
特点:到目前为止,还没有人求得一种最好的增量序列(gap),数组中不同个数的数值对应最好的增量序列也不同。
时间复杂度:
空间复杂度:O(1) 。
稳定性:不稳定。
思路:选择排序是为了将数组的最小的元素调整到数组的最前方,并且是从0下标开始调整。
public static void selectSort(int[] arr) {
for (int i = 0; i <arr.length ; i++) {
for (int j = i+1; j <arr.length ; j++) {
if(arr[i]>arr[j]) {
int tmp = arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
}
}
}
选择排序特点:每一次从无序区间选出最大(或最小)的一个元素,存放在无序区
间的最后(或最前),直到全部待排序的数据元素排完 。
时间复杂度:O(N^2) 。
空间复杂度:O(1) 。
稳定性:不稳定。
下面是优化的代码,设置了flg能够直接判断数组是否是有序的,若在一个排序的情况下已知整个数组是有序的,则直接退出循环,不用再继续排序了。
思路:外层遍历的是数组需要排序的趟数,而j遍历的是数组当中的每个数据是否需要排序。
需要注意的是,i和j的范围都是array.length-1
,是因为调整到数组的最后时,数组的最后一个数的前面的数已经是调整过的了,并且最后一个数无需再调整。
public static void bubbleSort(int[] array) {
// boolean flg = false;
for (int i = 0; i < array.length-1; i++) {
boolean flg = false;//为什么每一趟都要置为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 == false) {
break;
}
}
}
特点:冒泡排序比较直观,若前面一个数比后面一个数大,则直接交换。
时间复杂度:最好最坏都是O(n^2) ,但是:如果优化了,并且数组有序的时候就是O(n),只要看情况。
空间复杂度:O(1) 。
稳定性:稳定。
原理:基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆。
下面这篇博客是作者早前详细写过有关堆排序以及它的应用的文章,有兴趣的可以点入链接,此处不再说明。
堆排序及堆的应用
特点:堆排序能够很快地找出堆当中前K大或前K小的数据。
时间复杂度:O(N*log2^N) 。
空间复杂度:O(1) 。
稳定性:不稳定。
原理:待排序的数据当中要找到一个基准(pivot)。
Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;找基准也是一个排序的过程。
采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度==1,代表已经有序,或者小区间的长度 = = 0,代表没有数据。
挖坑法代码思路(partition):
以数组0下标的元素作为基准,存入tmp中。设置low指向0下标,high指向数组的最后的下标。
如果有arr[high]>=tmp
,则high向左直到遇到有比tmp大的。如果high向左遍历中有比tmp小的,则arr[low]=arr[high]
。
此时向右遍历low,判断是否有数据比tmp大。若有,则arr[high]=arr[low]。
当然,如果low和high相遇了就不再遍历low和high。此时将tmp的值放入arr[low]中即可,此时返回low下标就是数组的基准,它是将数组划分为左右两边,左边的数都是比它小的,右边的数都是比它大的。挖坑法首先将数组0下标的数放入tmp中,此时一定要从右边开始与tmp比较大小。
图解:
初始化:
挖坑法后的第一趟:
此时6已经是排好序的,只要以相同的挖坑法去执行pivot左右两边的数据即可达到排序。
三数取中找基准是有arr[mid]<=arr[low]<=arr[high]
是为了将基准最后按照挖坑法放到数组的中间位置,更快地递归执行pivot两边的数据。
快速排序的递归:
public static void insertSort(int[] arr,int start,int end) {
for (int i = start+1; i <arr.length ; i++) {
int tmp = arr[i];
int j = i-1;
for (j = i-1; j >=start ; j--) {
if(arr[j]>tmp) {
arr[j+1]=arr[j];
}else {
break;
}
}
arr[j+1]=tmp;
}
}
public static int partition(int[] arr,int low,int high) {
int tmp = arr[low];
while(low<high) {
while(low<high&&tmp<=arr[high]) {
high--;
}
arr[low]=arr[high];
while(low<high&&tmp>=arr[low]) {
low++;
}
arr[high]=arr[low];
}
arr[low]=tmp;
return low;
}
public static void quickSort(int[] arr,int low,int high) {
if(low>=high) {
return;
}
if(high-low+1<=100) {
insertSort(arr,low,high);
}
selectPivotMedianOfThree(arr,low,high);
int pivot = partition(arr,low,high);
quickSort(arr,0,pivot-1);
quickSort(arr,pivot+1,high);
}
//快排中的三数取中找基准
public static void selectPivotMedianOfThree(int[] arr,int low,int high) {
//arr[mid]<=arr[low]<=arr[high]
int mid = (low+high)/2;
if(arr[mid]>arr[low]) {
int tmp = arr[mid];
arr[mid]=arr[low];
arr[low]=tmp;
}
if(arr[low]>arr[high]) {
int tmp = arr[low];
arr[low]=arr[high];
arr[high]=tmp;
}
}
public static void quick(int[] arr) {
quickSort(arr,0,arr.length-1);
}
public static void main(String[] args) {
int[] array = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
quick(array);
System.out.println(Arrays.toString(array));
}
快速排序非递归要用到栈。
思路:首先找到数组的基准。并且要判断基准的两边是否足够数据来找基准。若数据个数为1或0,则无需找基准;反之可以找基准。
再将左边数据的左右边界压入栈中,再将右边数据的左右边界压入栈中。此时栈如果不为空,则出栈中的两个元素,即右边数据的左右边界,此时再去找它们的基准。若右边的找基准的左右边界不再入栈即右边完成了排序。同样地,左边找基准也是如此。
代码:
public static int partition(int[] arr,int low,int high) {
int tmp = arr[low];
while(low<high) {
while(low<high&&tmp<=arr[high]) {
high--;
}
arr[low]=arr[high];
while(low<high&&tmp>=arr[low]) {
low++;
}
arr[high]=arr[low];
}
arr[low]=tmp;
return low;
}
public static void quickSort2(int[] array) {
Stack<Integer> stack = new Stack<>();
int start = 0 ;
int end= array.length-1;
int pivot = partition(array,start, end);
if(pivot>start+1) {
stack.push(0);
stack.push(pivot-1);
}
if(pivot<end-1) {
stack.push(pivot+1);
stack.push(end);
}
while(!stack.empty()) {
end =stack.pop();
start =stack.pop();
pivot = partition(array,start,end);
if(pivot>start+1) {
stack.push(0);
stack.push(pivot-1);
}
if(pivot<end-1) {
stack.push(pivot+1);
stack.push(end);
}
}
}
注意:出栈的先后顺序分别是end和start 。
思路:
1.先调用partition,找到pivot。
2.把当前的pivot的左边区间和右边区间的下标放入栈中。
3.判断栈是否为空,不为空弹出两个元素,要注意先后顺序给的是谁。
4.再进行partition,若左右都排完序则栈为空。
特点:排序速度最快。
时间复杂度:
最好时间复杂度:O(n * log2^N) 。 基准将数组均匀地分割。如同一颗二叉树。
最坏时间复杂度:O(N^2) 。 数组完全逆序。 如同单分支的二叉树。
空间复杂度:
最好空间复杂度:O(log2^N) 。
最坏空间复杂度:O(N) 。
稳定性:不稳定。
表示最好时间复杂度。
表示最坏时间复杂度:
如:9,8,7,6
原理与思路:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
从上往下的归并排序采用了递归的方式实现。它的原理非常简单,如图:
以上图为例,很像一个二叉树,将一个数组的每一个元素分为一组,这是“递”。再将一个一个合并为每两个一组、两个两个合并为四个一组等等,这是"归"。**因此此处涉及到了合并两个有序数组的问题。**观察下图,可见当low和high相等时,则子数据不需要再进一步归并排序了,此时只需要将它们合并并且排序。要注意的是需要设置一个数组tmp将数据都保存起来。
因为看图解,归并排序的分解很像一颗二叉树的先序遍历。当low和high相等就是一个单位的数据,此时就要归并。
public static void mergeSort1(int[] array) {
mergeSortInternal(array, 0,array.length-1);
}
public static void mergeSortInternal(int[] array,int low,int high) {
if(low >= high) {
return;
}
int mid = (low+high) / 2;
mergeSortInternal(array,low,mid);
mergeSortInternal(array,mid+1,high);
//合并的过程
merge(array,low,mid,high);
}
public static void merge(int[] array,int low,int mid,int high) {
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
int[] tmp = new int[high-low+1];
int k = 0;//代表tmp数组的下标
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
//有2种情况
while (s1 <= e1){
//说明第2个归并段没有了数据 把第1个归并段剩下的数据 全部拷贝过来
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
//说明第1个归并段没有了数据 把第2个归并段剩下的数据 全部拷贝过来
tmp[k++] = array[s2++];
}
//tmp数组当中 存储的就是当前归并好的数据
//这样的代码是错误的
/*for (int i = 0; i < array.length; i++) {
array[i] = tmp[i];
}*/
for (int i = 0; i < tmp.length; i++) {
array[i+low] = tmp[i];
}
}
注意:
1.tmp数组的作用就是将分散的数据合并起来,因此有array[i+low] = tmp[i];
**是因为数组的后半段不能覆盖掉数组前面的数据,**若array[i] = tmp[i];
就会覆盖掉前面的数据。
如:后面的40 80与20 60归并在一起时,不能覆盖掉原数组前面10 30 50 70 90的值。并且此时它们的low为5,array[i] = tmp[i];
能够避免。
2.递归实现的归并排序首先每一个每一个合并为一组,此时一组为两个,再每两组每两组合并,此时一组为四个,以此类推。
非递归来实现归并排序跟递归实现归并排序的方法类型,也要**考虑到合并两个有序的数组问题。**但不同的是,非递归是直接将一整个数组运用递归的归并排序思想的基础上在原数组当中调整。因此tmp数组的长度直接设置为原数组的长度。
注意:
要考虑到右边是否有归并段,因此有while循环的判断条件s2 < array.length
。如果右边没有归并段,则直接将左边归并段的数据拷入到tmp当中,tmp数组的大小跟原数组的大小相同。如果右边有归并段,则归并完成后要调整s1,e1,s2,e2的值。
mergeSort方法内部是控制每几个每几个数据的合并。merge2方法内部是将两个数组合并并排序。
e2的位置要注意不能数组越界,如果超过数组的长度,则让它指向数组最后的元素;若不超过,则按照常规逻辑有e2=s2+gap-1
。
public static void mergeSort(int[] array) {
for (int i = 1; i < array.length; i*=2) {
merge2(array,i);
}
}
public static void merge2(int[] array,int gap) {
int[] tmp = new int[array.length];
int k = 0;
int s1 = 0;
int e1 = s1+gap-1;
int s2 = e1+1;
int e2 = s2+gap-1 < array.length ? s2+gap-1 : array.length-1;
//保证有两个归并段
while (s2 < array.length) {
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
//一组完了 确定新的区间的开始和结束
s1 = e2+1;
e1 = s1+gap-1;
s2 = e1+1;
e2 = s2+gap-1 < array.length ? s2+gap-1 : array.length-1;
}
//e2 > array.length
while (s1 <= array.length-1) {
tmp[k++] = array[s1++];
}
for (int i = 0; i < tmp.length; i++) {
array[i] = tmp[i];
}
}
特点:归并排序的排序速度相对来说比较快,但仍没有快速排序快。并且它的空间复杂度较大。
时间复杂度: 最好最坏情况下:O(N * log2^N) 。
空间复杂度:O(N) 。
稳定性:稳定。
我正在尝试对每个条目有多个值的关联数组进行排序。 例如 [0] => stdClass Object ( [type] => node [sid] => 158 [score] => 0.059600
我在 mysql 中有“日期”列以这种格式保存日期 2014 年 9 月 17 日(日-月-年) 我需要对它们进行升序排序,所以我使用了这个命令: SELECT * FROM table ORDER
我目前正在将 MySQL 存储过程重写为 MS SQL 存储过程,但遇到了问题。 在 MySQL 存储过程中,有一个游标,它根据最近的日期 (effdate) 选择一个值并将其放入变量 (thestt
我想要 gwt r.QuestionId- 排序。但是我得到未排序的 QuestionId 尽管我提到了 QuestionId ASC 的顺序。 SELECT r.QuestionId,
我有一个关于在 scandir 函数中排序的基本问题。到目前为止,我阅读了 POSIX readdir 的手册页,但没有找到有关订购保证的具体信息。 但是当我遍历大目录(无法更改,只读)时,我在多个系
基本上我必须从 SQL 数据库中构建项目列表,但是用户可以选择对 7 个过滤器的任意组合进行过滤,也可以选择要排序的列以及按方向排序。 正如您可以想象的那样,这会以大量不同的组合进行编码,并且数据集非
我有两张 table 。想象第一个是一个目录,包含很多文件(第二个表)。 第二个表(文件)包含修改日期。 现在,我想选择所有目录并按修改日期 ASC 对它们进行排序(因此,最新的修改最上面)。我不想显
我想先根据用户的状态然后根据用户名来排序我的 sql 请求。该状态由 user_type 列设置: 1=活跃,2=不活跃,3=创始人。 我会使用此请求来执行此操作,但它不起作用,因为我想在“活跃”成员
在 C++ 中,我必须实现一个“类似 Excel/Access”(引用)的查询生成器,以允许对数据集进行自定义排序。如果您在 Excel 中使用查询构建器或 SQL 中的“ORDER BY a, b,
我面临这样的挑战: 检索按字段 A 排序的文档 如果字段 B 存在/不为空 . 否则 按字段排序 C. 在 SQL 世界中,我会做两个查询并创建一个 UNION SELECT,但我不知道如何从 Mon
我想对源列表执行以下操作: map 列表 排序 折叠 排序 展开 列表 其中一些方法(例如map和toList)是可链接的,因为它们返回非空对象。但是,sort 方法返回 void,因为它对 List
我制作了一个用于分析 Windows 日志消息编号的脚本。 uniq -c 数字的输出很难预测,因为根据数字的大小会有不同的空白。此时,我手动删除了空白。 这是对消息进行排序和计数的命令: cat n
我有以下词典: mydict1 = {1: 11, 2: 4, 5: 1, 6: 1} mydict2 = {1: 1, 5: 1} 对于它们中的每一个,我想首先按值(降序)排序,然后按键(升序)排序
我刚刚开始使用泛型,目前在对多个字段进行排序时遇到问题。 案例: 我有一个 PeopleList 作为 TObjectList我希望能够通过一次选择一个排序字段,但尽可能保留以前的排序来制作类似 Ex
有没有办法在 sql 中组合 ORDER BY 和 IS NULL 以便我可以在列不为空时按列排序,但如果它为null,按另一列排序? 最佳答案 类似于: ORDER BY CASE WHEN
我有一个包含 2 列“id”和“name”的表。 id 是常规的自动增量索引,name 只是 varchar。 id name 1 john 2 mary 3 pop 4 mary 5 j
场景 网站页面有一个带有分页、过滤、排序功能的表格 View 。 表中的数据是从REST API服务器获取的,数据包含数百万条记录。 数据库 REST API 服务器 Web 服务器 浏览器 问
假设我有一本字典,其中的键(单词)和值(分数)如下: GOD 8 DONG 16 DOG 8 XI 21 我想创建一个字典键(单词)的 NSArray,首先按分数排序,然后按字
如何在 sphinx 上通过 sql 命令选择前 20 行按标题 WEIGHT 排序,接下来 20 行按标题 ASC 排序(总共 40 个结果),但不要给出重复的标题输出。 我尝试了这个 sql 命令
我有一个奇怪的问题,当从 SQLite 数据库中选择信息并根据日期排序时,返回的结果无效。 我的SQL语句是这样的: Select pk from usersDates order by dateti
我是一名优秀的程序员,十分优秀!