一、排序算法
排序基础
1.排序算法的稳定性
2.何为原地排序算法
1.冒泡排序
从下面第一个元素开始往上冒泡,一轮冒泡下来,最大的元素就去了最上面了
步骤:无序数组
每次冒泡都可以将最大的元素放到最右边去
第一轮比较了5次:
第二轮比较了4次:
第三轮比较了3次:
。。。
第四轮比较了2次
。。。
有6个元素比较了6次,第一次比较了5次,第二次4次。。。
code:
两次遍历,第一次控制冒泡的轮数,第二次控制每次比较次数
public class BubbleSorter {
public void sort(int[] data){
if(data == null || data.length <=1) return;
for (int round = 1; round <= data.length; round++) { //控制冒泡的轮数
int compareTimes = data.length - round;
for (int i = 0; i < compareTimes; i++) { //控制每轮比较次数
if(data[i]>data[i+1]){
int tmp = data[i];
data[i] = data[i+1];
data[i+1] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] data = new int[]{12,23,36,9,24,42};
new BubbleSorter().sort(data);
System.out.println(Arrays.toString(data));
}
}
特点
- 时间复杂度分析:
时间复杂度:O(n²)
- 空间复杂度为O(1),原地排序算法
- 冒泡排序是稳定的排序算法
只有大于才进行交换,两个36的位置不会改变
优化:减少元素交换次数
在第二轮中已经是有序的了,就不需要再进行第三轮、第四轮交换了
代码实现
public class BubbleSorter {
public void sort(int[] data){
if(data == null || data.length <=1) return;
for (int round = 1; round <= data.length; round++) { //控制冒泡的轮数
boolean hasSwap = false;
int compareTimes = data.length - round;
for (int i = 0; i < compareTimes; i++) { //控制每轮比较次数
if(data[i]>data[i+1]){
int tmp = data[i];
data[i] = data[i+1];
data[i+1] = tmp;
hasSwap = true;
}
}
//如果没有发生交换就可以退出了
if(!hasSwap){
break;
}
}
}
public static void main(String[] args) {
int[] data = new int[]{12,23,36,9,24,42};
new BubbleSorter().sort(data);
System.out.println(Arrays.toString(data));
}
}
2. 选择排序
重在选择,每次选择出来一个最小的元素和前面的元素进行交换
选择排序的步骤:
1.找到数组中最小的数组元素
2.将这个最小的数组元素和数组第一个元素进行交换
3.在剩下元素中找到最小的数组元素和数组第二个元素进行交换
。。。
假设第一个元素是最小值,在剩下元素里面找最小值,还需要一个指针 j 来遍历数组找最小值,和 i 进行比较
遍历到12的时候,12比第一个元素值小,可能是最小值,将其索引放到minNumIndex中
第一轮遍历完成后就可以找到数组中最小的元素值的索引,存放在minNumIndex中,将这个索引指向的元素和指针i 指向的元素进行交换
第二轮:
第三轮:
第三轮,找到最小元素值的索引为4,将其和 i 指针指向元素进行交换
。。。
数组有多少个元素就需要进行多少轮的排序
时间复杂度:O(n²)
code:
将交换元素的方法抽取为父类
public class Sorter {
public void swap(int[] nums,int i,int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
代码实现
public class SelectionSort extends Sorter{
public void sort(int[] data){
if(data == null || data.length<=1) return;
for (int i = 0; i < data.length; i++) { //控制选择排序的轮数
//找到[i,n)中的最小元素所在的索引 minNumIndex用来存放最小值的索引
int minNumIndex = i;
//j从i+1开始遍历数组,找到最小元素值
for (int j = i+1; j < data.length; j++) {
if(data[j]<data[minNumIndex]){
minNumIndex = j;
}
}
//将data[i]和最小元素进行交换
swap(data,i,minNumIndex);
}
}
public static void main(String[] args) {
int[] data = new int[]{12,23,36,9,24,42};
new SelectionSort().sort(data);
System.out.println(Arrays.toString(data));
}
}
特点
- 空间复杂度:O(1),原地排序算法 时间复杂度:O(n²)
- 选择排序是不稳定的排序算法
3. 插入排序
将每一个元素插入到已经有序的数组的位置
核心思想:取未排序区间的元素在已排序区间中找到合适的插入位置将其插入,并保证已排序区间的数据一直有序,重复这个过程直到未排序区间没有元素为止
一般是假设第一个元素为已排序元素
插入排序的步骤:有n个元素就进行n轮
第一轮:取第一个元素34,这个元素已经有序
第二轮:
从第二个元素开始,对比 j 指向元素和 j-1 元素,如果比 j-1 元素值要小,则进行交换
第三轮:从第三个元素开始,比前一个元素,位置正确不用动
第四轮:从第四个元素开始,比前一个元素小,要向前移动
进行交换,注意此时,j-1 和 j 需要继续向前移动
第五轮:
第六轮:
拿到未排序区间的元素,一直往前找,找到合适的位置将这个元素插入进去,一直到未排序区间为空,就表示这个数组排序完成了
时间复杂度:
遍历一遍数组,时间复杂度为O(n)
每轮遍历需要将一个元素插入到有序数组的正确位置这个插入过程的最坏时间复杂度为O(n)
根据乘法法则:O(n²)
code:
代码实现(优化前)
public class InsertionSorter extends Sorter{
public void sort(int[] data){
if(data==null||data.length<=1)return;
for (int i = 1; i < data.length; i++) { //控制插入排序的轮数,第一个元素已经默认有序,可以存第二个开始
for (int j = i; j > 0 ; j--) { //j从i开始向前移动,和j-1进行判断,j-1不能小于0,j-1也不能等于0,不然就变成data[-1]了
if(data[j] < data[j-1]){
swap(data,j,j-1);
}else{
break;
}
}
}
}
public static void main(String[] args) {
int[] data = new int[]{12,23,36,9,24,42};
new InsertionSorter().sort(data);
System.out.println(Arrays.toString(data));
}
}
特点
- 空间复杂度是O(1),时间复杂度为O(n²),原地排序算法
- 插入排序是稳定的排序算法,可以选择不去进行交换,只有小于的时候才去交换
代码实现(优化后)
插入排序的优化:
不使用元素交换,使用赋值代替元素交换,减少元素访问次数
可以将有序数组中较大的元素总是向右移动,然后将元素插入到正确的位置
//优化
public void sort1(int[] data){
if(data==null||data.length<=1)return;
for (int i = 1; i < data.length; i++) { //控制插入排序的轮数,第一个元素已经默认有序,可以存第二个开始
int tmp = data[i];
int j;
for (j = i; j > 0 ; j--) { //j从i开始向前移动,和j-1进行判断,j-1不能小于0,j-1也不能等于0,不然就变成data[-1]了
if(tmp < data[j-1]){
//将较大的元素向后移动
data[j] = data[j-1];
}else{
break;
}
}
//找到i 对应的元素需要插入的位置
data[j] = tmp;
}
}
冒泡、选择、插入排序性能对比
时间复杂度都是O(n²)
通过比较交换次数来比较性能
package com.douma.line.algo.sort;
import java.util.Random;
/**
* 对比冒泡、选择、插入排序性能
*/
public class SortCompare {
private static Random random = new Random();
private static int[] getData(int n){
int[] data = new int[n];
for (int i = 0; i < n; i++) {
data[i] = random.nextInt();
}
return data;
}
private static long time(String sortType,int[] data){
long start = System.currentTimeMillis();
if(sortType.equals("bubble"))new BubbleSorter().sort(data);
else if(sortType.equals("selection"))new SelectionSort().sort(data);
else if(sortType.equals("insertion"))new InsertionSorter().sort(data);
return System.currentTimeMillis() - start;
}
private static long manyTimeSort(String sortType,int n,int k){
long totalTime = 0;
for (int i = 0; i < k; i++) {
totalTime += time(sortType,getData(n));
}
return totalTime;
}
public static void main(String[] args) {
double t1 = manyTimeSort("bubble",1000,100);
double t2 = manyTimeSort("selection",1000,100);
double t3 = manyTimeSort("insertion",1000,100);
System.out.println(t1 / t2); //t1 > t2
System.out.println(t2 / t3); //t2 > t3
//结论:插入排序性能最好(稳定),其次是选择排序(不稳定),最后是冒泡排序(稳定)
}
}
推荐使用插入排序,性能最好,是稳定的
插入排序有两种实现方式,一种是基于交换实现的;另一种是基于赋值实现的;赋值的效率要比交换效率要高
4. 希尔排序
什么是希尔排序?
是对插入排序的优化
如果有一个大规模乱序数组插入排序很慢,因为它只会交换相邻的两个元素
这个大规模数组在进行若干次插入排序后,前面已经有序了,最后一个是最小的元素,这个时候需要将最小元素一步步的向前比较移动,
这个过程会非常慢
希尔排序就可以解决大规模数组插入排序慢的问题
希尔排序的思想:
先使数组中任间隔为h(4)的元素有序,然后对全局(也就是h=1)进行排序
希尔排序递增序列计算公式
间隔h的取值逻辑
最常用的是:
递增序列的取值:
使用公式计算得出h取值,k取1,2,3,4,5带入公式计算出h取值,h应该小于N/3
public class ShellSorter {
public void sort(int[] data){
if(data == null || data.length <=1)return;
//1.计算递增序列
int n = data.length;
ArrayList<Integer> list = new ArrayList<>();
int k = 1;
int h;
do{
h = (int)(Math.pow(3,k)-1)/2;
if(h > n/3)break;
list.add(h); // 1,4,13,40,121....
k++;
}while (h <= n/3);
}
}
倒序遍历递增序列
//2.希尔排序
for (k = list.size()-1; k >=0 ; k--) { //倒序遍历递增序列
h = list.get(k);
// 将数组变为 h 有序
}
希尔排序详细步骤
希尔排序过程 h = 4,经过此次排序,间隔为4的元素就局部有序了
全局排序 h =1
代码实现
public class ShellSorter extends Sorter{
public void sort(int[] data){
if(data == null || data.length <=1)return;
//1.计算递增序列
int n = data.length;
ArrayList<Integer> list = new ArrayList<>();
int k = 1;
int h;
do{
h = (int)(Math.pow(3,k)-1)/2;
if(h > n/3)break;
list.add(h); // 1,4,13,40,121....
k++;
}while (h <= n/3);
//2.希尔排序
for (k = list.size()-1; k >=0 ; k--) { //倒序遍历递增序列
h = list.get(k);
// 将数组变为 h 有序
for (int i = h; i < n; i++) { //倒序遍历
for (int j = i; j >= h ; j=j-h) {
if(data[j] < data[j-h]){
swap(data,j,j-h);
}else{
break;
}
}
}
}
}
public static void main(String[] args) {
int[] data = new int[]{12,23,36,9,24,42};
new ShellSorter().sort(data);
System.out.println(Arrays.toString(data));
}
}
希尔排序对比插入排序性能
性能要比插入排序好,具体要看递增序列h的取值,根据公式获取
特点
希尔排序的特点
- 空间复杂度:O(1),原地排序算法
- 希尔排序是不稳定的排序算法