Java中排序算法主要包括交换排序、插入排序、选择排序、归并排序、以及基数排序。交换排序包括冒泡排序和快速排序,插入排序包括直接插入排序和希尔排序,选择排序包括直接选择排序和堆排序。
1.冒泡排序
步骤:
- 比较相邻的元素。如果第一个比第二个小,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最小的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public static void bubbleSort(int[] arr){
int temp;
boolean flag;
for(int i = 0;i < arr.length-1;i++){ //控制趟数
flag = false; //定义标记变量减少循环次数
for(int j = 0;j <arr.length-1-i;j++ ){
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true;
}
}
if(!flag){
return;
}
}
}
public static <T>void bubbleSort(T[] arr,Comparator<T> c){
T temp;
boolean flag;
for(int i = 0;i < arr.length-1;i++){
flag = false;
for(int j = 0;j < arr.length-1-i;j++){
if(c.compare(arr[j],arr[j+1]) > 0){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true;
}
}
if(!flag){
return;
}
}
}
2.选择排序
步骤:
- 在未排序序列中找到最小元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小元素,然后放到排序序列起始位置。
- 以此类推,直到所有元素均排序完毕。
public static void chooseSort(int[] arr){
int index,temp;
for(int i = 0;i < arr.length-1;i++){
index = i;
for (int j = i+1;j < arr.length;j++){
if(arr[index] > arr[j]){
index = j;
}
}
if(index != i){
temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
}
3.插入排序
步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后面的位置
- 重复步骤2
public static void insertSort(int[] arr){
int temp;
for(int i = 1;i < arr.length;i++){
temp = arr[i];
int j = i-1;
for(;j >=0;j--){
if(arr[j] > temp){
arr[j+1] = arr[j];
}
else {
break;
}
}
arr[j+1] = temp;
}
}
4.希尔排序
希尔排序也是一种插入排序,它是直接插入排序的一种算法改进方式。希尔排序的时间复杂度相比直接插入排序的时间复杂度要小。他与直接插入排序的不同在于它会优先比较距离较远的元素。希尔排序是按照一定的增量进行分组排序,对每一组进行直接插入排序,随着分组个数的减少,每组中的元素就会越来越多,当增量减少为 1 时,排序结束。
步骤:
选择增量 gap=length/2;缩小增量继续以 gap=gap/2 的方式进行分组。{n/2,(n/2)/2,(n/2)/4,…,1}增量(gap(步长)必须为奇数,若为偶数则+1)
- 选择一个增量序列,按照增量序列个数 m,进行 m 趟排序。
- 每趟排序根据对应的增量次数分别进行元素的分组操作,对组内进行直接插入排序操作。
- 继续下一个增量,分别进行分组直接插入操作。
- 重复步骤③,直到增量变成 1,所有元素在一个分组内,希尔排序结束(增量为1时,仍要排序)
public static void shellSort(int[] arr){
int step = arr.length/2;
while(step != 0){
for(int k = 0;k < step;k++){
for(int i = k+step;i < arr.length;i += step){
int temp = arr[i];
int j = i-step;
for(;j >= k ;j -= step){
if(temp < arr[j]){
arr[j+step] = arr[j];
}
else{
break;
}
}
arr[j+step] = temp;
}
}
step = step/2;
if(step%2 == 0 && step != 0){
step++;
}
}
}
5.快速排序
快速排序主要是通过选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。依此递归,达到总体待排序序列都有序。
- 一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。
- 找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。
- 直到从前往后比较数组下标大于从后往前比较的数组下标值,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。
- 接着分别比较左右两边的序列,重复上述的循环。
private static int quickSortOfOnce(int[] arr,int left ,int right){ //一次快速排序
int flag = arr[left];
while (left < right){
while (arr[right] >= flag && left < right){
right--;
}
if(left == right){
break;
}
else {
arr[left] = arr[right];
}
while (arr[left] <= flag && left < right){
left++;
}
if(left == right){
break;
}
else {
arr[right] = arr[left];
}
}
arr[left] = flag;
return left;
}
public static void quickSort(int[] arr,int left ,int right){ //递归函数实现
int index = quickSortOfOnce(arr,left,right); //获取上次排序结束后,基准所在位置下标
if(index-left >= 2){ //基准左边至少有两个元素,<2(即只有一个时)排序捷顺
quickSort(arr,left,index-1);
}
if(right-index >= 2){
quickSort(arr,index+1,right);
}
}
public static void quickSort(int[] arr,int left, int right){
int flag = arr[left];
int l = left;
int r = right;
while (left < right){
while (arr[right] >= flag && left < right){
right--;
}
if(left == right){
break;
}
else {
arr[left] = arr[right];
}
while (arr[left] <= flag && left < right){
left++;
}
if(left == right){
break;
}
else {
arr[right] = arr[left];
}
}
arr[left] = flag;
if(left-l >= 2){ //基准左边至少有两个元素,<2(即只有一个时)排序捷顺
quickSort(arr,l,left-1);
}
if(r-left >= 2){
quickSort(arr,left+1,r);
}
}
快速排序算法分析:
- 快速排序的时间性能取决于快速排序递归的深度,可以用递归数来描述算法的执行情况。
- 如果递归树是平衡的,那么此时的性能也是最好的。在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。
- 就空间复杂度来说,主要是递归造成的堆空间的使用,最好情况,递归树的深度 log2n ,其空间复杂度也就为 O(logn) ,最坏情况,需要进行递归调用,其空间复杂度为 O(n),平均情况空间复杂度也为 (logn)。
- 关键字的比较和交换是跳跃进行的,因此,快速排序是种不稳定的排序方法。
快速排序优化算法:
- 随机选取基准
- 三数取中:
即取三个关键字先进行排序,将中间数作为基准, 一般是取左端、右端和中间三个数。将三个数进行排序,将中间的值放到第一个 low 位置,作为基准进行比较。
对于非常大的待排序的序列来说还是不足以保证能够选择出一个好的基准, 因此还有个办法是所谓的九数取中,先从数组中分三次取样,每次取三个数,三个样品各取出中数,然后从这三个中数当中再取出一个中数作为基准。 - 优化小数组(数组元素少)
快速排序适用于非常大的数组的解决办法, 那么相反的情况,如果数组非常小,其实快速排序反而不如直接插入排序来得更好(直接插入是简单排序中性能最好的)。其原因在于快速排序用到了递归操作,在大量数据排序时,这点性能影响相对于它的整体算法优势是可以忽略的,但如果数组只有几个记录需要排序时,这就成了大材小用,因此我们需要改进一下 quick()函数。我们增加了一个判断, high-low 不大于某个常数时(有资料认为 7 较合适,认为 5 更合理理,实际应用可适当调整) ,就用直接插入排序,这样就能保证最大化地利用两种排序的优势来完成排序。
6.归并排序(二路归并)
步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
public static int[] mergeSortOnce(int[]arr,int gap) { //一次归并,gap为路的长度
int left1 = 0;
int right1 = left1+gap-1;
int left2 = right1+1;
int right2 = left2+gap-1 > arr.length-1 ? arr.length-1 : left2+gap-1;
int[] newArray = new int[arr.length];
int j = 0; //标记newArray
//有两个归并段(两路)
while(left2 < arr.length){
while (left1 <= right1 && left2 <= right2){
if(arr[left1] <= arr[left2]){
newArray[j++] = arr[left1++];
}
else {
newArray[j++] = arr[left2++];
}
}
//剩余第二路,从第二路left2 到 right2之间进行拷贝,拷贝到newArray
if(left1 > right1){
for(int i = left2;i <= right2;i++){
newArray[j++] = arr[i];
}
}
//剩余第一路,从第一路left1 到 right1之间进行拷贝,拷贝到newArray
if(left2 > right2){
for(int i = left1;i <= right1;i++){
newArray[j++] = arr[i];
}
}
//跳到下一组二路
left1 = right2+1;
right1 = left1+gap-1;
left2 = right1+1;
right2 = left2+gap-1 > arr.length-1 ? arr.length-1 : left2+gap-1;
}
//只有一路
for(int i = left1;i < arr.length;i++){
newArray[j++] = arr[i];
}
return newArray;
}
public static int[] mergeSort(int[] arr){
for(int gap = 1;gap <= arr.length;gap = gap*2){
arr = mergeSortOnce(arr,gap);
}
return arr;
}
7.堆排序
大根堆
- 建堆
- 堆调整(从下(倒数第一个非叶子节点)往上调整为大根堆)——> 建大根堆
- 当前大根堆堆顶元素和堆中相对的最后一个元素进行交换
- 缩范围,每次交换完确定一个元素位置,然后只需重复调整堆顶元素为大根堆
public static void adjust(int[] arr,int begin,int end){ //调整一个非叶子节点为大根堆
int temp = arr[begin];
for(int i = begin*2+1;i <= end;i = i*2+1){
if((i+1)<=end && arr[i]<arr[i+1]){ //先判断该节点是否有右孩子节点,然后左右孩子节点比较,&&是短路运算符,所以(i+1)<=end 必须位于&&左边
i = i+1; //i保存当前节点左右孩子的最大值
}
if(arr[i] > temp) {
arr[begin] = arr[i];
begin = i;
}
else {
break;
}
}
arr[begin] = temp;
}
public static void heapSort(int[] arr){
//建大根堆
for(int i = (arr.length-1-1)/2;i >= 0;i--){
adjust(arr,i,arr.length-1);
}
for(int i = 0;i < arr.length;i++){
int temp = arr[arr.length-1-i]; //堆顶元素和相对最后一个元素交换位置
arr[arr.length-1-i] = arr[0];
arr[0] = temp;
adjust(arr,0,arr.length-1-i-1); //建大根堆之后,只需重复调整堆顶元素为大根堆
}
}
排序算法空间复杂度、时间复杂度以及稳定性总结
排序算法稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
选择排序算法准则
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。