插入排序
每一步将一个待排序的元素,按照其排序码的大小,插入到前面已经排好序的一组元素的合适位置上去,直到元素全部插完为止。
当插入第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.维护lastSorted
为链表的已排序部分的最后一个结点,初始时lastSorted=head
4.维护cur
为待插入的元素,初始时cur = cur->next
;
5.比较lastSorted
和curr
的结点值
- 若
lastSorted.val <= cur.val
,说明cur
应该在lastSorted
之后,将lastSorted
后移一位,cur
变成心的lastSorted
. - 否则,从单链表的头结点开始往后遍历链表中的结点,寻找插入cur的位置。令prev为插入cur的位置的前一个结点,完成对cur的插入:
lastSorted->next = cur->next;
cur->next = pre->next;
pre->next = cur;
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;
}
};
元素集合越接近有序,直接插入排序算法的时间效率越高
插入排序的过程不会破坏原有数组中相同关键字的相对次序,所以插入排序是一种稳定的排序算法。
最优情况下:时间效率为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>Dm−1>Dm−2>...>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;
}
}
}
}
实际上,这段代码可以优化一下。我们现在的处理方式是:处理完一组间隔序列后,再回来处理下一组间隔序列,这非常符合人类思维。但对于计算机来说,它更喜欢从第 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;
}
}
}
希尔排序与 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) 的魔咒,此后的快排、堆排算法也都是基于这样的思路实现的。