java 数据结构 排序算法

目录

排序

插入排序

直接插入排序

希尔排序( 缩小增量排序 ):

直接选择排序

堆排序

交换排序

冒泡排序

快速排序递归

Hoare法

挖坑法

前后指针法

快速排序优化

快速排序非递归

归并排序

归并排序非递归

排序算法复杂度及稳定性分析

计数排序


排序(Sort)

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来

的操作,所有的排序默认都是从小到大排序

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录

的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]

之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:例如定义了一个数组,在哪开辟的呢?当程序运行起来在内存上开辟的,在内存中存储

的,那我们叫内部排序

外部排序:例如你要排序的数据在磁盘上存储着,那叫外部排序,例如如果有10个G数据,你的运行

内存只有8个G,只能借助外部来进行排序

常见的排序算法


插入排序

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的

记录插入完为止,得到一个新的有序序列 实际中我们玩扑克牌时,就用了插入排序的思想

直接插入排序

思路:设置两个变量记录,设置一个temp变量存储i下标的元素,j从下标为i-1的开始,i从下标为1

的开始,i递增,j递减,每次temp跟j下标的进行比较,如果小于,j+1下标的位置放入temp,如果大于

就不变还是放在i下标的位置

代码实现:

 //直接插入排序
    //适用于:带排序已经基本趋于有序
    //稳定性:稳定的,如果是本身就是稳定的排序,一定可以实现不稳定的排序
    public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int temp = array[i];
            int j = i - 1;
            for (; j >= 0; j--) {
                //这里加不加等号 和稳定有关系
                if (temp < array[j]) {
                    array[j + 1] = array[j];
                } else {
                    //array[j + 1] = temp;
                    break;
                }
            }
            array[j + 1] = temp;
        }
    }

疑问:

1.为什么array[j + 1] = array[j];j+1不能写i?当i=5,j=4,循环一次后,如果满足条件,这时候i还是5我们要改的是j+1下标

2.为什么array[j + 1] = temp;j+1不能写i?因为内部循环j结束后会递减,会到-1下标

直接插入排序的特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定


希尔排序( 缩小增量排序 ):

先进行分组,然后进行插入排序,希尔排序,gap就是组数

图解

思路:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为gap的记录分在同一组

内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有

记录在统一组内排好序。

代码实现:

 //希尔排序
    //稳定性:不稳定
    public static void shellSort(int[] array) {
        int gap = array.length;
        while (gap > 1) {
            gap /= 2;
            shell(array, gap);
        }
    }
    /**
     * 对每组进行插入排序
     *
     * @param array
     * @param gap
     */
    public static void shell(int[] array, int gap) {
        //为什么不是i+=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;
        }
    }

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。

当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

小结:希尔排序的速度比直接插入排序的速度快


直接选择排序

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部

待排序的数据元素排完 

图解

思路:设置一个变量minIndex用来记录i元素的下标,一开始存放的是i下标,然后跟j比较,j小的话

就更新里面的下标,内部循环结束后交换元素,因为内部比完了才知道哪个最小,i++,以此类推

代码实现:

 /**
     * 选择排序
     * 时间复杂度:O(n^2)
     * 空间复杂度:
     * 稳定性:
     *
     * @param array
     */
    public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            //记录i元素的下标
            int minIndex = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[j] < array[minIndex]) {
                    //如果j元素比i元素小,更新minIndex里面的下标
                    minIndex = j;
                }
            }
            swap(array, i, minIndex);
        }
    }
    public static void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

第二种方法实现

图解

思路:设置left和right变量,一个从第一个开始一个从最后一个开始,minIndex和maxIndex一开始

都是left,从第二个元素开始遍历,如果小于minIndex的值,我们就更新minIndex的下标,如果大于

的话我们更新maxIndex的下标,循环结束后,我们交换,把左边的元素都放小的,右边元素放大

注意:在交换右边元素的时候要注意如果一开始第一个是最大值,交换过后,如图中的9就到了

maxIndex的位置,28到了minIndex的位置,然后轮到maxIndex交换的时候这个9就跑到最后去了

代码实现:

 public static void selectSort2(int[] array) {
        int left = 0;
        int right = array.length - 1;
        while (left < right) {
            int minIndex = left;
            int maxIndex = left;
            for (int i = left + 1; i <= right; i++) {
                if (array[i] < array[minIndex]) {
                    minIndex = i;
                }
                if (array[i] > array[maxIndex]) {
                    maxIndex = i;
                }
            }
            swap(array, left, minIndex);
            //防止 第一个是最大值 如果和最小的一换 最大值就跑到了原来最小值的位置
            if (maxIndex == left) {
                maxIndex = minIndex;
            }
            swap(array, right, maxIndex);
            left++;
            right--;
        }
    }

直接选择排序的特性总结

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:不稳定

小总结:直接插入排序和选择排序的区别,直插设置变量存的是元素,选择存的是下标

直插可以直接进行交换,而选择要更新变量存的下标,得出最小值下标进行交换


堆排序

我在上一篇文章有讲解 java 数据结构 优先级队列(PriorityQueue)-CSDN博客


交换排序

冒泡排序

优化版实现:

 //冒泡排序
    public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean flg = false;
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                    flg = true;
                }
            }
            if (flg == false) {
                break;
            }
        }
    }

冒泡排序的特性总结:

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:稳定


快速排序递归

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序

列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,

直到所有元素都排列在相应位置上为止

缺点:数据多的时候会数据溢出,处理逆序不太好

Hoare法

图解

思路:Hoare法就是right从右边开始找比基准值小的,左边开始找比基准值大的,两个都满足就交

换,相遇了的时候就把相遇下标的元素跟基准值下标的元素进行交换

完整思路:这个做法先把最左边的作为基准值,然后尾巴找出比基准值小的元素然后停住,头找出

比基准值大的元素然后停住,两个进行交换,然后尾接着找比基准值小的元素,头接着找比基准值

大的元素,直到相遇,然后和基准值交换位置,这时候基准值的左边都是比它小的,右边都是比它

大的,然后递归,左边从头到基准值的位置-1开始,右边从基准值+1到尾开始,以此类推,直到,

头大于尾巴开始归,跟前序与中序的归一样,这里是大于等于就归

代码实现:

 //快速排序
    //Hoare版
    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }
    public static void quick(int[] array, int start, int end) {
        //如果大于等于,说明找完了
        if (start >= end) {
            return;
        }
        //找出基准值
        int div = quickNeedle(array, start, end);
        quick(array, start, div - 1);
        quick(array, div + 1, end);
    }
     public static int partitionHoare(int[] array, int left, int right) {
        int tmp = array[left];
        //这个i用来记录第一个下标元素
        //因为最后要跟第一个下标元素进行交换
        int i = left;
        while (left < right) {
            //这里要判断会不会越界
            //为什么要加=,因为不加会死循环
            while (left < right && array[right] >= tmp) {
                right--;
            }
            while (left < right && array[left] <= tmp) {
                left++;
            }
            //走到这说明左边找到比基准值大的,右边找到比基准值小的
            swap(array, left, right);
        }
        swap(array, i, left);
        //这里说明走到一起了,返回谁都行
        return left;
    }

挖坑法

图解

思路:先把第一个下标的元素挖出来存到一个变量里,然后开始循环,右边开始找如果找到比左边

小的,我们就把那个坑填上,然后更新一下坑的位置,然后开始左边如果找到比第一次存的元素大

的,我们去把刚才的坑填上,然后更新一下坑的位置,以此类推,最后左右相遇,把坑填上存一开

始存的第一个下标的元素,也就是我们的基准值

代码实现:

 //挖坑法
    public static int partitionHole(int[] array, int left, int right) {
        int tmp = array[left];
        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            //把坑填上
            array[left] = array[right];
            while (left < right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        //走到一起的时候这个坑空着 把坑填上我们的基准值
        array[left] = tmp;
        return tmp;
    }

疑问:

这两个问题,第一个问题如果少了等于(<=tmp)如果第一个元素和最后一个元素一样的话,会

死循环。

第二个为什么不能从左边开始,下图和上图对比一下,上图可以看出左边开始的话left

和right会停留在6这个位置,9出现在第一个位置这就不符合左边的都是小于基准值

的,排序也不能变有序。

小总结:

Hoare法就是right从右边开始找比基准值小的,左边开始找比基准值大的,两个都满足就交换,相

遇了的时候就把相遇下标的元素跟基准值下标的元素进行交换

挖坑法就是先把基准值挖掉,然后从右边开始找比基准值小的,找到就填入一开始基准值的坑,填

了这个坑那右边这个位置不就空了,然后左边找比基准值大的,找到就填刚才右边空的坑,以此类

推,然后相遇了的时候,这个位置的坑填入基准值


前后指针法

图解

思路:设置一个变量prev记录left下标也就是上面图中key的位置,然后设置一个变量cur记录left+1

的下标,while循环当cur走到比数组长度长就停止,然后每次cur下标的元素跟left下标也就是最左

边的元素进行比较,如果小于并且prev走到下一步所对应的下标元素跟cur的元素相同,cur和prev

元素就进行交换,否则cur往前走,循环结束后,prev停留的位置的元素跟最左边也就是left的元素

进行交换

代码实现:

 //前后指针法
    public static int quickNeedle(int[] array, int left, int right) {
        int prev = left;
        //cur从头下标+1开始走
        int cur = left + 1;
        //当cur走出right结束
        while (cur <= right) {
            //cur下标元素要小于头下标元素 并且
            if (array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array, prev, cur);
            }
            //没有cur就往前走
            cur++;
        }
        //走完了 prev下标的元素和头下标的元素进行交换 这个就是我们的基准值
        swap(array, prev, left);
        return prev;
    }

快速排序优化

1. 三数取中法选key

2. 递归到小的子区间时,可以考虑使用插入排序

思路:就比如说1-10这个有序的数组,把头和尾拿出来,中间的下标等于头尾相加/2,然后求中间

值,然后把求出的中间值的下标的元素跟头下标的元素进行交换,这时候头就是我们的基准值了

找出中间值下标

代码实现:

 public static void quick(int[] array, int start, int end) {
        //如果大于等于,说明找完了
        if (start >= end) {
            return;
        }
        //优化
        if (end - start + 1 <= 15) {
            //使用直接插入排序
            insertSort(array, start, end);
        }
        //优化,三数求中法
        int mid = middle(array, start, end);
        //找出中间值下标和头下标的元素进行交换
        swap(array, start, mid);
        //找出基准值
        int div = quickNeedle(array, start, end);
        quick(array, start, div - 1);
        quick(array, div + 1, end);
    }
    //方法的重载
    public static void insertSort(int[] array, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int temp = array[i];
            int j = i - 1;
            for (; j >= left; j--) {
                //这里加不加等号 和稳定有关系
                if (temp < array[j]) {
                    array[j + 1] = array[j];
                } else {
                    //array[j + 1] = temp;
                    break;
                }
            }
            array[j + 1] = temp;
        }
    }
    //找出中间值的下标
    public static int middle(int[] array, int left, int right) {
        //求中间的下标
        int mid = (left + right) / 2;
        //找中间值
        if (array[left] < array[right]) {
            if (array[mid] < array[left]) {
                return left;
            } else if (array[mid] > array[right]) {
                return right;
            } else {
                return mid;
            }
        } else {
            //array[left] > array[right]
            if (array[mid] < array[right]) {
                return right;
            } else if (array[mid] > array[left]) {
                return left;
            } else {
                return mid;
            }
        }
    }

快速排序非递归

思路:需要用到栈,首先设置头和尾的下标,然后找基准值调用Hoare法或者挖坑法都行,然后判

断左边元素有几个,如果基准值下标-1大于我们的头下标,就push进栈要push头下标和基准值下

标,如果相等说明左边就一个元素,然后判断右边也是一个道理如果基准值下标+1小于我们的尾

下标,就push进栈要push基准值下标和尾下标,然后while循环判断我们的栈空不空,不空就出右

边下标和左边下标,然后找基准值再调用我Hoare法或者挖坑法,然后再次判断左右元素有几个,

以此为循环

代码实现:

 //快速排序非递归
    public static void quickSort2(int[] array) {
        int start = 0;
        int end = array.length - 1;
        Stack stack = new Stack<>();
        int div = partitionHoare(array, start, end);
        if (div - 1 > start) {
            stack.push(start);
            stack.push(div - 1);
        }
        if (div + 1 < end) {
            stack.push(div + 1);
            stack.push(end);
        }
        //栈不为空,弹出两个下标
        while (!stack.isEmpty()) {
            end = stack.pop();
            start = stack.pop();
            //接着整理
            div = partitionHoare(array, start, end);
            if (div - 1 > start) {
                stack.push(start);
                stack.push(div - 1);
            }
            if (div + 1 < end) {
                stack.push(div + 1);
                stack.push(end);
            }
        }
    }

快速排序总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(logN)

4. 稳定性:不稳定


归并排序

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若

将两个有序表合并成一个有序表,称为二路归并

图解

分解思路:先设置头和尾,然后找出中间下标,然后递归左边以头和中间下标开始递归,右边以中

间下标+1和尾开始递归,当递归到头和尾下标一样,走到一起了开始返回,计算出mid后,传入第

一个式子的end就是mid,然后递归,mid更新,以此类推

怎么递?怎么归?怎么合并?

递归到只有单个元素的时候归,然后合并,合并完归到上一个式子的(mid+1,end)方法接着分

解到只有单个元素,然后合并以此类推

合并思路:写一个方法,记录刚才分解的元素的下标,也可以不记录,记录下来好理解一点,然后创建一个数组,长度是最右边下标-最左边下标+1,然后开始合并

代码实现:

 //归并排序
    public static void mergeSort(int[] array) {
        mergeSort(array, 0, array.length - 1);
    }
    private static void mergeSort(int[] array, int start, int end) {
        if (start >= end) {
            return;
        }
        //递归分解
        int mid = (start + end) / 2;
        mergeSort(array, start, mid);
        mergeSort(array, mid + 1, end);
        //合并
        merge(array, start, mid, end);
    }
    //合并
    private static void merge(int[] array, int left, int mid, int right) {
        //这些都可以不定义,方便理解
        int s1 = left;
        int e1 = mid;
        int s2 = mid + 1;
        int e2 = right;
        //创建一个新的数组
        int[] tmpArr = new int[right - left + 1];
        //从0下标开始存
        int k = 0;
        while (s1 <= e1 && s2 <= e2) {
            //比较元素大小
            if (array[s1] <= array[s2]) {
                tmpArr[k++] = array[s1++];
            } else {
                tmpArr[k++] = array[s2++];
            }
        }
        //走到这 说明还有数据没存完
        while (s1 <= e1) {
            tmpArr[k++] = array[s1++];
        }
        while (s2 <= e2) {
            tmpArr[k++] = array[s2++];
        }
        //拷贝给原来的数组
        for (int i = 0; i < tmpArr.length; i++) {
            //为什么+left? 不然右边也是从0下标拷贝 会覆盖掉原来的
            array[i + left] = tmpArr[i];
        }
    }

归并排序非递归

思路:先从1个开始调整有序,然后2个,然后4个,然后8个,但其实1个就是有序的

设置变量gap从1开始,代表每组几个数据,就像上面说的从1个开始逐渐到8个

设置for循环i从0下标开始,每次递增i+=gap*2,表示每次调整数据每一组2个,随着gap的增加,

每组4个,然后每组8个,就是每次分2个一组,然后合并成有序的,然后2个2个都合并并且有序

了,然后gap增加,然后每组4个4个合并并且有序,以此类推

注意:如果是偶数个这样写没什么问题,但是如果奇数个这样写,mid和right可能会越界,所以当

mid和right越界的时候,把它调整到数组长度-1的下标,然后调整合并

代码实现:

 //归并排序非递归
    public static void mergeSortNor(int[] array) {
        //每组(gap)几个数组
        int gap = 1;
        while (gap < array.length) {
            for (int i = 0; i < array.length; i += gap * 2) {
                int left = i;
                int mid = left + gap - 1;
                int right = mid + gap;
                //越界问题
                if (mid >= array.length) {
                    mid = array.length - 1;
                }
                if (right >= array.length) {
                    right = array.length - 1;
                }
                //合并
                merge(array, left, mid, right);
            }
            gap *= 2;
        }
    }

归并排序总结:

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(N)

4. 稳定性:稳定


排序算法复杂度及稳定性分析


计数排序

不完全思路:比如0-9的数字乱序给它进行排序,创建一个数字,下标从0-9,这个数组也可以叫计

数数组,遍历0-9的数字,遍历到0,计数数0下标+1,还是0继续+1,此时0下标对应的就是2,代

表有2个,没有出现的话就标记0,以此类推,遍历完后,然后输出就变成有序的了

修改思路:但是这是0-9的数字,所以不行,得考虑大的数字,所以要设置一个最大最小值,通过

遍历array数值得出最大最小值,然后count数组的长度是大小值相减+1,然后遍历array数组,

count[array[i]-minval]++,通过这样对出现的数据,在count数组的i下标进行计数,然后还要重写 写

回到原来的数组,for遍历count数组,然后while循环判断count数组下标存的数字的次数是不是大于

0,大于0才有,小于等于都是没有的,然后还要设置一个index=0,array从0下标开始存,array[index]=i+minval,然后下标++,count[i]- -,count数组下标的次数--

代码实现:

 //计数排序 不用比较大小进行排序
    public static void countSort(int[] array) {
        int minVal = array[0];
        int maxVal = array[0];
        //获取array数组中的最大和最小值
        for (int i = 1; i < array.length; i++) {
            if (array[i] < minVal) {
                minVal = array[i];
            }
            if (array[i] > maxVal) {
                maxVal = array[i];
            }
        }
        //确定计数数组长度
        int len = maxVal - minVal + 1;
        int[] count = new int[len];
        //遍历array数组  array数组元素出现的次数存放到计数数组存放
        for (int i = 0; i < array.length; i++) {
            //array数组的元素-最小值
            //一开始count数组的每个下标存放的都是0
            count[array[i] - minVal]++;
        }
        int index = 0;
        for (int i = 0; i < count.length; i++) {
            //count数组i下标的元素要大于0
            while (count[i] > 0) {
                //重写 写回array数组 从0下标开始
                array[index] = i + minVal;
                index++;
                count[i]--;
            }
        }
    }

以上便是基本的排序算法,欢迎阅读