冒泡&选择&堆&插入

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

冒泡排序

冒泡排序可能是我们大部分人接触的第一个排序方法,也是最最最基础的算法了。

原理

比较两个相邻的元素,将值大的元素交换到右边

思路

依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

    (1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

    (2)比较第2和第3个数,将小数 放在前面,大数放在后面。

    ……

    (3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

    (4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

    (5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

    (6)依次类推,每一趟比较次数减少依次
image.png
(2)第一趟排序:

      第一次排序:10和1比较,10大于1,交换位置   [1,10,35,61,89,36,55]

      第二趟排序:10和35比较,10小于35,不交换位置  [1,10,35,61,89,36,55]

      第三趟排序:35和61比较,35小于61,不交换位置  [1,10,35,61,89,36,55]

      第四趟排序:61和89比较,61小于89,不交换位置  [1,10,35,61,89,36,55]

     第五趟排序:89和36比较,89大于36,交换位置   [1,10,35,61,36,89,55]

      第六趟排序:89和55比较,89大于55,交换位置   [1,10,35,61,36,55,89]

      第一趟总共进行了六次比较,排序结果:[1,10,35,61,36,55,89]

for (int end = array.length - 1; end > 0; end--) {
            for (int begin = 1; begin <= end; begin++) {
                if (array[begin] < array[begin - 1]) {
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                }
            }
        }
复制代码

算法分析:

    (1)由此可见:N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次,所以可以用双重循环语句,外层控制循环多少趟,内层控制每一趟的循环次数

    (2)冒泡排序的优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。如上例:第一趟比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,没进行一趟比较,每一趟少比较一次,一定程度上减少了算法的量。

优化1:(只能在数组前部部分有序的情况下优化)

如果经过一次排序后,剩下未排序的数字已经排列好,无需剩下的多次循环排序;则我们应设定变量来控制终止剩下的循环。

for (int end = array.length - 1; end > 0; end--) {
            boolean sorted = true;
            for (int begin = 1; begin <= end; begin++) {
                if (array[begin] < array[begin - 1]) {
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                    sorted = false;
                }
            }
            if (sorted) break;
        }
复制代码

优化2:(数组中有有序部分优化)

在数组中,有着片段的子数组是有序的,在排序中我们不需要对他进行排列,可以将他视为一个整体进行排列;如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数 。
image.png
最后1次交换的位置是6

示例:

for (int end = array.length - 1; end > 0; end--) {
            // sortedIndex的初始值在数组完全有序的时候有用
            int sortedIndex = 1;
            for (int begin = 1; begin <= end; begin++) {
                if (array[begin] < array[begin - 1]) {
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                    sortedIndex = begin;
                }
            }
            end = sortedIndex;
        }
复制代码

算法分析:

最坏、平均时间复杂度:O(n2); 最好时间复杂度:O(n) ;空间复杂度:O(1) ;

优化之后冒泡排序,能快速地将数组内,已经排好序的子数组,不经子数组内部排序,将子数组当成一个整体进行排序;在某个数组中有多个已排好序的子数组,在时间运算上有极大的提升。

如果相等的2个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法

排序前:            5, 1,3?, 4,7,3?

稳定的排序:     1,3?, 3?, 4,5,7

不稳定的排序: 1, 3?, 3?,4, 5,7  

对自定义对象进行排序时,稳定性会影响最终的排序效果  

冒泡排序属于稳定的排序算法 稍有不慎,稳定的排序算法也能被写成不稳定的排序算法,比如下面的冒泡排序代码是不稳定的
image.png

选择排序

在我看来选择排序的冒泡排序有着异曲同工之妙,两者的区别是冒泡排序是遇到比自己大的就进行一次交换,而选择排序是找到此次排序中的最大值之后再进行一次交换,减少了不必要的交换次数。

原理:

1.从序列中找出最大的那个元素,然后与最末尾的元素交换位置 执行完一轮后,最末尾的那个元素就是最大的元素;

2.忽略 1 中曾经找到的最大元素,重复执行步骤 1;

示例:

for (int end = array.length - 1; end > 0; end--) {
            int maxIndex = 0;
            for (int begin = 1; begin <= end; begin++) {
                if (array[maxIndex] <= array[begin]) {
                    maxIndex = begin;
                }
            }
            int tmp = array[maxIndex];
            array[maxIndex] = array[end];
            array[end] = tmp;
        }
复制代码

算法分析:

选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序 ;

最好、最坏、平均时间复杂度:O(n2),空间复杂度:O(1),属于不稳定排序 ;

\

堆排序

堆排序的排序方式可以看成选择排序的优化,在未排序部分利用堆的方式来寻找最大值。

原理:

执行流程 :① 对序列进行原地建堆(heapify); ② 重复执行以下操作,直到堆的元素数量为 1 ;交换堆顶元素与尾元素 ;堆的元素数量减 1 ;对 0 位置进行 1 次 siftDown 操作
image.png
image.png
image.png
image.png
示例:

        // 原地建堆
        heapSize = array.length;
        for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
            siftDown(i);
        }
        
        while (heapSize > 1) {
            // 交换堆顶元素和尾部元素
            swap(0, --heapSize);

            // 对0位置进行siftDown(恢复堆的性质)
            siftDown(0);
        }


private void siftDown(int index) {
        T element = array[index];
        
        int half = heapSize >> 1;
        while (index < half) { // index必须是非叶子节点
            // 默认是左边跟父节点比
            int childIndex = (index << 1) + 1;
            T child = array[childIndex];
            
            int rightIndex = childIndex + 1;
            // 右子节点比左子节点大
            if (rightIndex < heapSize && 
                    cmp(array[rightIndex], child) > 0) { 
                child = array[childIndex = rightIndex];
            }
            
            // 大于等于子节点
            if (cmp(element, child) >= 0) break;
            
            array[index] = child;
            index = childIndex;
        }
        array[index] = element;
    }
复制代码

算法分析:

相较于选择排序,在选取最大值时利用了堆的数据结构,降低了复杂度,因为是利用数组原地进行建堆,所以空间复杂度为O(1)

最好、最坏、平均时间复杂度:O(nlogn),空间复杂度:O(1),属于不稳定排序 ;

插入排序

 插入排序非常类似于扑克牌的排序
image.png
原理

① 在执行过程中,插入排序会将序列分为2部分 头部是已经排好序的,尾部是待排序的 

② 从头开始扫描每一个元素 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
image.png
示例:

        for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            while (cur > 0 && cmp(cur, cur - 1) < 0) {
                swap(cur, cur - 1);
                cur--;
            }
        }
    /*
     *cmp
     * 返回值等于0,代表 array[i1] == array[i2]
     * 返回值小于0,代表 array[i1] < array[i2]
     * 返回值大于0,代表 array[i1] > array[i2]
     *
     *swap
     *交换
     */
复制代码

\

\

插入排序——逆序对

  • 什么是逆序对?

数组 <2,3,8,6,1> 的逆序对为:<2,1> <3,1> <8,1> <8,6> <6,1>,共5个逆序对。

image.png

算法分析:

最坏、平均时间复杂度:O(n2)

最好时间复杂度:O(n)

空间复杂度:O(1)

属于稳定排序  

当逆序对的数量极少时,插入排序的效率特别高 甚至速度比 O nlogn 级别的快速排序还要快

数据量不是特别大的时候,插入排序的效率也是非常好的

插入排序——优化

原理:

 思路是将【交换】转为【挪动】

① 先将待插入的元素备份

② 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置

③ 将待插入元素放到最终的合适位置 

image.png

示例:

for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            T v = array[cur];
            while (cur > 0 && cmp(v, array[cur - 1]) < 0) {
                array[cur] = array[cur - 1];
                cur--;
            }
            array[cur] = v;
        }
复制代码

插入排序——二分搜索优化

插入排序就像在已排列好的数据中,插入一个数值合适,位置合适的数,而我们寻找这个位置的时候就可以使用二分查找(在我的另一篇文章)来找到哪个合适的位置。

原理:

在元素 v 的插入过程中,可以先二分搜索出合适的插入位置,然后再将元素 v 插入 

image.png

要求二分搜索返回的插入位置:第1个大于 v 的元素位置

如果 v 是 5,返回 2;

如果 v 是 1,返回 0;

如果 v 是 15,返回 7 ;

如果 v 是 8,返回 5
假设在 [begin, end) 范围内搜索某个元素 v,mid == (begin + end) /2

如果 v < m,去 [begin, mid) 范围内二分搜索

如果 v ≥ m,去 [mid + 1, end) 范围内二分搜索 

image.png

image.pngimage.png

image.png

image.png
示例:

//排序方法
protected void sort() {
        for (int begin = 1; begin < array.length; begin++) {
            insert(begin, search(begin));
        }
    }
    
    /**
     * 将source位置的元素插入到dest位置
     * @param source
     * @param dest
     */
    private void insert(int source, int dest) {
        T v = array[source];
        for (int i = source; i > dest; i--) {
            array[i] = array[i - 1];
        }
        array[dest] = v;
    }
    
    /**
     * 利用二分搜索找到 index 位置元素的待插入位置
     * 已经排好序数组的区间范围是 [0, index)
     * @param index
     * @return
     */
    private int search(int index) {
        int begin = 0;
        int end = index;
        while (begin < end) {
            int mid = (begin + end) >> 1;
            if (cmp(array[index], array[mid]) < 0) {
                end = mid;
            } else {
                begin = mid + 1;
            }
        }
        return begin;
    }
复制代码

算法分析:

需要注意的是,使用了二分搜索后,只是减少了比较次数,但插入排序的平均时间复杂度依然是 O(n2) ;

我们只是在原有的复杂度上在查找合适位置时,进行了优化原有的时间复杂度没有变化。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享