当前位置:   article > 正文

插入排序与希尔排序_希尔排序可以用链表吗

希尔排序可以用链表吗

插入排序

每一步将一个待排序的元素,按照其排序码的大小,插入到前面已经排好序的一组元素的合适位置上去,直到元素全部插完为止。

当插入第i (i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
在这里插入图片描述
数组的插入排序:

vector<int> sortArray(vector<int>& nums) {
    int len = nums.size();
    //从第二个数字开始 往前插入数字
    for(int i = 1; i < len; i++) {
        int cur = nums[i];
        int j = i - 1;
        //寻找插入位置的过程中 不断的将比cur大的数字往后挪
        while(j >= 0 && cur < nums[j]) {
            nums[j + 1] = nums[j];   //元素后移
            j--;
        }
        //两种情况跳出循环:
        //(1)遇到一个小于或者等于cur的数字 跳出循环 cur就插入到它前面
        //(2)已经走到数组头 仍然找不到 此时j == -1 cur 就插入到数组头部
        nums[j + 1] = cur;
    }
    return nums;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

单链表的插入排序:

1.首先判断给定的链表是否为空,若为空,则不需要进行排序,直接返回
2.创建哑结点
3.维护lastSorted为链表的已排序部分的最后一个结点,初始时lastSorted=head
4.维护cur为待插入的元素,初始时cur = cur->next;
5.比较lastSortedcurr的结点值

  • lastSorted.val <= cur.val,说明cur应该在lastSorted之后,将lastSorted后移一位,cur变成心的lastSorted.
  • 否则,从单链表的头结点开始往后遍历链表中的结点,寻找插入cur的位置。令prev为插入cur的位置的前一个结点,完成对cur的插入:
lastSorted->next = cur->next;
cur->next = pre->next;
pre->next = cur;
  • 1
  • 2
  • 3

6.令cur = lastSorted->next,此时cur为下一个待插入的元素.
7.重复步骤5和6,直到cur为空
8.返回dummy->next;

class Solution {
public:
    ListNode* insertionSortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* lastSorted = head;
        ListNode* curr = head->next;
        while (curr != nullptr) {
            if (lastSorted->val <= curr->val) {
                lastSorted = lastSorted->next;
            } else {
                ListNode *prev = dummyHead;
                while (prev->next->val <= curr->val) {
                    prev = prev->next;
                }
                lastSorted->next = curr->next;
                curr->next = prev->next;
                prev->next = curr;
            }
            curr = lastSorted->next;
        }
        return dummyHead->next;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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

插入排序的过程不会破坏原有数组中相同关键字的相对次序,所以插入排序是一种稳定的排序算法。

最优情况下:时间效率为O(N)

最差情况下:时间复杂度为O(N2)

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

希尔排序

希尔排序又称缩小增量排序,是对插入排序的优化:

  • 将待排序数组按照一定的间隔分为多个子数组,每组分别进行插入排序。这里按照间隔分组指的不是取连续的一段数组,而是每跳跃一个值组成一组
  • 逐渐缩小间隔进行下一轮排序
  • 最后一轮时,取间隔为1,也就是相当于直接使用插入排序。但这时经过前面的排序,数组已经基本有序,因此此时的插入排序只需要进行少量交换便可以完成。

举个例子,对数组 [84, 83, 88, 87, 61, 50, 70, 60, 80, 99] 进行希尔排序的过程如下:

  • 第一遍(5间隔排序):

按照间隔 5分割子数组,共分成五组,分别是
[84, 50], [83, 70], [88, 60], [87, 80], [61, 99]

对它们进行插入排序,排序后它们分别变成:
[50, 84], [70, 83], [60, 88], [80, 87], [61, 99]

此时整个数组变成 [50, 70, 60, 80, 61, 84, 83, 88, 87, 99]

  • 第二遍(2 间隔排序):

按照间隔 2 分割子数组,共分成两组,分别是
[50, 60, 61, 83, 87], [70, 80, 84, 88, 99]

对他们进行插入排序,排序后它们分别变成:[50, 60, 61, 83, 87], [70, 80, 84, 88, 99]

此时整个数组变成 [50, 70, 60, 80, 61, 84, 83, 88, 87, 99]

这里有一个非常重要的性质:当我们完成 2 间隔排序后,这个数组仍然是保持 5 间隔有序的。也就是说,更小间隔的排序没有把上一步的结果变坏。

  • 第三遍(1 间隔排序,等于直接插入排序)

按照间隔 1 分割子数组,分成一组,也就是整个数组。对其进行插入排序,经过前两遍排序,数组已经基本有序了,所以这一步只需经过少量交换即可完成排序。排序后数组变成 [50, 60, 61, 70, 80, 83, 84, 87, 88, 99],整个排序完成。

其中,每一遍排序的间隔在希尔排序中被称为增量,所有的增量组成的序列称为增量序列,也就是本例中的[5,2,1].增量依次递减,最后一个增量必须为1,所以希尔排序又被称之为[缩小增量排序]

以专业术语来描述希尔排序,可以分为以下两个步骤:

  • 定义增量序列 D m > D m − 1 > D m − 2 > . . . > D 1 = 1 D_m > D_{m-1} > D_{m-2} > ... > D_1 = 1 Dm>Dm1>Dm2>...>D1=1
  • ​对每个 D k D_k Dk进行 「 D k D_k Dk间隔排序」

有一条非常重要的性质保证了希尔排序的效率:

D k + 1 D_{k+1} Dk+1间隔」 有序的序列,在经过 「 D k D_k Dk间隔」 排序后,仍然是 「 D k + 1 D_{k+1} Dk+1间隔」 有序的

void shellSort(vector<int>& arr) {
    // 间隔序列,在希尔排序中我们称之为增量序列
    for (int gap = arr.size() / 2; gap > 0; gap /= 2) {
        // 分组
        for (int groupStartIndex = 0; groupStartIndex < gap; groupStartIndex++) {
            // 插入排序
            for (int currentIndex = groupStartIndex + gap; currentIndex < arr.size(); currentIndex += gap) {
                // currentNumber 站起来,开始找位置
                int currentNumber = arr[currentIndex];
                int preIndex = currentIndex - gap;
                while (preIndex >= groupStartIndex && currentNumber < arr[preIndex]) {
                    // 向后挪位置
                    arr[preIndex + gap] = arr[preIndex];
                    preIndex -= gap;
                }
                // currentNumber 找到了自己的位置,坐下
                arr[preIndex + gap] = currentNumber;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

实际上,这段代码可以优化一下。我们现在的处理方式是:处理完一组间隔序列后,再回来处理下一组间隔序列,这非常符合人类思维。但对于计算机来说,它更喜欢从第 gap 个元素开始,按照顺序将每个元素依次向前插入自己所在的组这种方式。虽然这个过程看起来是在不同的间隔序列中不断跳跃,但站在计算机的角度,它是在访问一段连续数组。

void shellSort(vector<int>& arr) {
    // 间隔序列,在希尔排序中我们称之为增量序列
    for (int gap = arr.size() / 2; gap > 0; gap /= 2) {
        // 从 gap 开始,按照顺序将每个元素依次向前插入自己所在的组
        for (int i = gap; i < arr.size(); i++) {
            // currentNumber 站起来,开始找位置
            int currentNumber = arr[i];
            // 该组前一个数字的索引
            int preIndex = i - gap;
            while (preIndex >= 0 && currentNumber < arr[preIndex]) {
                // 向后挪位置
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            // currentNumber 找到了自己的位置,坐下
            arr[preIndex + gap] = currentNumber;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

练习题目:相对名次

希尔排序与 O( n 2 n^2 n2) 级排序算法的本质区别:

相对于冒泡排序、选择排序、插入排序(时间复杂度为O( n 2 n^2 n2) )来说,希尔排序的排序过程显得较为复杂,接下来我们来分析一个有趣的问题:希尔排序凭什么可以打破时间复杂度 O( n 2 n^2 n2) 的魔咒呢?它和O( n 2 n^2 n2) 级排序算法的本质区别是什么?

这个问题我们可以用逆序对来理解:

当我们从小到大排序时,在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。

排序算法本质上就是一个消除逆序对的过程。

对于随机数组,逆序对的数量是 O( n 2 n^2 n2) 级的,如果采用「交换相邻元素」的办法来消除逆序对,每次最多只能消除一组逆序对,因此必须执行 O( n 2 n^2 n2) 级的交换次数,这就是为什么冒泡、插入、选择算法只能到 O( n 2 n^2 n2) 级的原因。反过来说,基于交换元素的排序算法要想突破 O( n 2 n^2 n2) 级,必须通过一些比较,交换间隔比较远的元素,使得一次交换能消除一个以上的逆序对。

希尔排序算法就是通过这种方式,打破了在空间复杂度为 O(1)的情况下,时间复杂度为 O( n 2 n^2 n2) 的魔咒,此后的快排、堆排算法也都是基于这样的思路实现的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/1000411
推荐阅读
相关标签
  

闽ICP备14008679号