[算法]“简简单单”的插入排序,你还没掌握吗?

写在前边

大家好,我是melo,一名大二上软件工程在读生,经历了一年的摸滚,现在已经在工作室里边准备开发后台项目啦。
不过这篇文章呢,还是想跟大家聊一聊数据结构与算法,学校也是大二上才开设了数据结构这门课,希望可以一边学习数据结构一边积累后台项目开发经验。
前几篇,我们都是聊了聊数据结构,也该来聊点新花样了,接触一些更考验思维和逻辑的算法,现在这一阶段的算法了,我们主要先来谈论的都是排序算法,这节就从最“简单"的插入排序开始吧!

思路

插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

总结就是: 把前n-1个看成有序了,然后把第n个插入到前边中的适当位置即可,成为新的有序表

更具体详细的思路过程都嵌入在代码的注释里了喔!

注意

哨兵a0相比insertValue的作用

  • 可以省去下边的 j >0,因为哨兵就意味着,你走到哨兵那个位置了,就自然要停下来了

最后是j+1才是位置

从后往前比较(注意第一个不满足就直接不继续遍历了)

力扣912排序数组

每个都判断然后再插入是一定会超时的,n平方 所以我们需要先判断最后一个,然后再看还用不用往下遍历

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* sortArray(int* nums, int numsSize, int* returnSize){
    //从第二个数开始遍历
    for(int i=1;i<numsSize;i++){
        //保存待插入的值
        int insertValue=nums[i];
        int insertIndex=0;
        for(int j=i-1;j>=0;j--){
            //若待插入的值小于索引值,证明要索引值需要后移,空出这个位置给插入值
            if(insertValue<nums[j]){
                nums[j+1]=nums[j];
                //记录待插入的位置
                insertIndex=j;
            }
        }
        //跳出循环后,把这个数插入到指定位置
        nums[insertIndex]=insertValue;
    }
    /*returnSize = (int*)malloc(numsSize*sizeof(int));
    for(int i=0;i<numsSize;i++){
        returnSize[i]=nums[i];
    }*/
    *returnSize=numsSize;
    return nums;
}

优化

for循环加了条件,insertValue<nums[j]

实际上因为前n-1个数已经有序了,所以如果我们待插入值还大于最后一个数时,则无须继续遍历下去了

image.png

最好情况:O(n)

原数组是升序的,每轮循环中只需要比较1次便进行下一轮的循环,一共需要比较O(n)次。

最差情况:O(n^2) (粗略)

原数组是降序的,数组中任意两个数都要比较一次,共比较O(n^2)次。

平均情况:O(n^2)

空间复杂度:O(1)

只用了一个哨兵位置

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* sortArray(int* nums, int numsSize, int* returnSize){
    int insertValue = 0;
    int j;
    //从第二个数开始遍历
    for(int i=1;i<numsSize;i++){
        //保存待插入的值
        insertValue=nums[i];
        //因为本身有序,若待插入的数还大于最后一个数,则无须继续遍历下去了
        for(j=i-1;j>=0 && insertValue<nums[j];j--){
            //若待插入的值小于索引值,证明要索引值需要后移,空出j这个位置给插入值
                nums[j+1]=nums[j];
        }
        //跳出循环后,把这个数插入到指定位置
        nums[j+1]=insertValue;
    }
    /*returnSize = (int*)malloc(numsSize*sizeof(int));
    for(int i=0;i<numsSize;i++){
        returnSize[i]=nums[i];
    }*/
    *returnSize=numsSize;
    return nums;
}

哨兵设置在0的方法

设置哨兵在0的话,可以省去j>0这一判断,因为走到哨兵自然会停下来

#include "allinclude.h"  //DO NOT edit this line
void InsertSort(RcdSqList &L)
{ // Add your code here
  for(int i=2;i<=L.length;i++){
    int j=i-1;
    //记录哨兵
    L.rcd[0].key=L.rcd[i].key;
    //找到要插入的位置(往哨兵的方向,哨兵在前边)
    while(L.rcd[j].key>L.rcd[L.length+1].key){
      L.rcd[j+1].key=L.rcd[j].key;
      j--;
    }
    //出循环时,j+1即是要插入的位置
    L.rcd[j+1].key=L.rcd[L.length+1].key;
  }
}

哨兵设置在length+1

因为我们要体现哨兵的作用,得体现出走到哨兵自然就停下来了的作用
原本我们上边是i从0到末尾,然后用一个j去从i前边一个元素遍历到哨兵的位置
现在我们哨兵放在后边了,我们得反过来思考。
让i从后往前遍历,这样j才能从前往后,遍历到哨兵的位置

  
//刚好跟上边相反,i是从后往前去遍历  
for(int i=L.length-1;i>=0;i--){
    int j=i+1;
    //若本身就小于后边一个了,则无须进行任何移动
    if(L.rcd[i].key>L.rcd[j].key){
      //记录哨兵
      L.rcd[L.length+1].key=L.rcd[i].key;
      //找到要插入的位置(j是往后去走,因为哨兵在后边)
      while(L.rcd[j].key<L.rcd[L.length+1].key){
        L.rcd[j-1].key=L.rcd[j].key;
        j++;
      }
      //出循环时,j-1即是要插入的位置
      L.rcd[j-1].key=L.rcd[L.length+1].key;
    }
  }

总结模板

  • for(从第二个开始遍历)

    • j=i-1; insertVal = a [i]
    • while(j>=0 && insertVal < a[j] )
  • a[j+1]=insertVal

链表版本

区别

其实数组跟链表的区别在于,数组可以方便的找到前一个,而链表不能,所以我们需要额外用一个pre指针来记录上一个,同时可以进行优化

力扣147对链表进行插入排序

https://leetcode-cn.com/problems/insertion-sort-list/

超时版本(老老实实从头往前找)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* insertionSortList(struct ListNode* head){
    struct ListNode* drumyHead=(struct ListNode*)malloc(sizeof(struct ListNode));
    drumyHead->next=head;
    //待插入元素p(直接从第二个元素开始)
    struct ListNode* p=drumyHead->next->next;
    while(p){
        struct ListNode* q=drumyHead;
        struct ListNode* pos=NULL;
        //若还未遍历到待插入元素的上一个位置
        while(q->next!=p){
            //若遍历到的当前元素的下一位大于待插入元素,则应将待插入元素插到当前元素的下一位
            if(q->next->val > p->val){
                //记录待插入位置
                pos=q;
                //得跳出阿!因为跟之前从后往前找不一样,这里是从前往后找,比如原本是 2 3 4 1
                //发现2>1,此时就已经是正确位置了,得跳出
                break;
            }
            //移动q
            q=q->next;
        }
        //再让q移动到p的前一个位置,方便操作
        while(q->next!=p){
            q=q->next;
        }
        //出循环时,q已经到待插入元素p的前一位
        if(pos!=NULL){
            //注意顺序
            //先让q的next=p的next
            q->next=p->next;
            p->next=pos->next;
            pos->next=p;
        }
        //移动p
        p=p->next;
    }
    return drumyHead->next;
}

用pre指针,每次遍历的时候记录一下前驱即可

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

struct ListNode* insertionSortList(struct ListNode* head){
    struct ListNode* drumyHead=(struct ListNode*)malloc(sizeof(struct ListNode));
    drumyHead->next=head;
    //待插入元素p(直接从第二个元素开始)
    struct ListNode* p=drumyHead->next->next;
    //记录待插入元素的前驱,记得初始化为第一个元素!!!
    struct ListNode* pre=drumyHead->next;
    while(p){
        //先跟前驱比较
        if(p->val>pre->val){
            pre=p;
            p=p->next;
            continue;
        }
        struct ListNode* q=drumyHead;
        struct ListNode* pos=NULL;
        //若还未遍历到待插入元素的上一个位置
        while(q->next!=p){
            //若遍历到的当前元素的下一位大于待插入元素,则应将待插入元素插到当前元素的下一位
            if(q->next->val > p->val){
                //记录待插入位置
                pos=q;
                //得跳出阿!因为跟之前从后往前找不一样,这里是从前往后找,比如原本是 2 3 4 1
                //发现2>1,此时就已经是正确位置了,得跳出
                break;
            }
            //移动q
            q=q->next;
        }
        //让q移动到p的前一个位置(因为前边break了),方便操作
        while(q->next!=p){
            q=q->next;
        }
        //出循环时,q已经到待插入元素p的前一位
        if(pos!=NULL){
            //注意顺序
            //先让q的next=p的next
            q->next=p->next;
            p->next=pos->next;
            pos->next=p;
        }
        //更新前驱
        pre=p;
        //移动p
        p=p->next;
    }
    return drumyHead->next;
}

耗时360ms左右

image.png

时间优化(判断前驱放在while而不要用continue)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

struct ListNode* insertionSortList(struct ListNode* head){
    struct ListNode* drumyHead=(struct ListNode*)malloc(sizeof(struct ListNode));
    drumyHead->next=head;
    //待插入元素p(直接从第二个元素开始)
    struct ListNode* p=drumyHead->next->next;
    //记录待插入元素的前驱,记得初始化为第一个元素!!!
    struct ListNode* pre=drumyHead->next;
    struct ListNode* q;
    struct ListNode* pos;
    while(p){
        // //先跟前驱比较(这样效率低了好多,这样要300ms,换成下边while判断只需要40ms)
        // if(p->val>pre->val){
        //     pre=p;
        //     p=p->next;
        //     continue;
        // }
        q=drumyHead;
        pos=NULL;
        //先判断是否小于前驱,不是的话直接不用进循环了!!!
        //若还未遍历到待插入元素的上一个位置
        while(q->next!=p&&p->val<pre->val){    
            //若遍历到的当前元素的下一位大于待插入元素,则应将待插入元素插到当前元素的下一位
            if(q->next->val > p->val){
                //记录待插入位置
                pos=q;
                //得跳出阿!因为跟之前从后往前找不一样,这里是从前往后找,比如原本是 2 3 4 1
                //发现2>1,此时就已经是正确位置了,得跳出
                break;
            }
            //移动q
            q=q->next;
        }
        //没必要了,因为有pre可以记录p的上一个位置了
        // //让q移动到p的前一个位置(因为前边break了),方便操作
        // while(q->next!=p){
        //     q=q->next;
        // }
        //出循环时,判断pos,得知是否需要插入
        if(pos!=NULL){
            //注意顺序
            //先让q的next=p的next
            pre->next=p->next;
            p->next=pos->next;
            pos->next=p;
        }
        //更新前驱
        pre=p;
        //移动p
        p=p->next;
    }
    return drumyHead->next;
}

耗时40ms左右

image.png

最外层放个if,时间最优(36ms)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

struct ListNode* insertionSortList(struct ListNode* head){
    struct ListNode* drumyHead=(struct ListNode*)malloc(sizeof(struct ListNode));
    drumyHead->next=head;
    //待插入元素p(直接从第二个元素开始)
    struct ListNode* p=drumyHead->next->next;
    //记录待插入元素的前驱,记得初始化为第一个元素!!!
    struct ListNode* pre=drumyHead->next;
    struct ListNode* q;
    struct ListNode* pos;
    while(p){
        // //先跟前驱比较
        // if(p->val>pre->val){
        //     pre=p;
        //     p=p->next;
        //     continue;
        // }
        //直接在这里进行比较,尽量不要continue
        if(p->val<pre->val){
            q=drumyHead;
            pos=NULL;
            //若还未遍历到待插入元素的上一个位置
            while(q->next!=p){
                
                //若遍历到的当前元素的下一位大于待插入元素,则应将待插入元素插到当前元素的下一位
                if(q->next->val > p->val){
                    //记录待插入位置
                    pos=q;
                    //得跳出阿!因为跟之前从后往前找不一样,这里是从前往后找,比如原本是 2 3 4 1
                    //发现2>1,此时就已经是正确位置了,得跳出
                    break;
                }
                //移动q
                q=q->next;
            }
            // //让q移动到p的前一个位置(因为前边break了),方便操作
            // while(q->next!=p){
            //     q=q->next;
            // }
            //出循环时,q已经到待插入元素p的前一位
            if(pos!=NULL){
                //注意顺序
                //先让q的next=p的next
                pre->next=p->next;
                p->next=pos->next;
                pos->next=p;
            }
        }
        //更新前驱
        pre=p;
        //移动p
        p=p->next;
        
    }
    return drumyHead->next;
}

image.png

Continue耗时测试

测试方法

调用下边的stamp方法,分别在插入排序前打印时间和插入排序后打印时间

#include <ctime>
#include <string>
#include <chrono>
#include <sstream>
#include <iomanip>
#include <iostream>
 
// use strftime to format time_t into a "date time"
std::string date_time(std::time_t posix)
{
    char buf[20]; // big enough for 2015-07-08 10:06:51\0
    std::tm tp = *std::localtime(&posix);
    return {buf, std::strftime(buf, sizeof(buf), "%F %T", &tp)};
}
 
std::string stamp()
{
    using namespace std;
    using namespace std::chrono;
 
    // get absolute wall time
    auto now = system_clock::now();
 
    // find the number of milliseconds
    auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;
 
    // build output string
    std::ostringstream oss;
    oss.fill('0');
 
    // convert absolute time to time_t seconds
    // and convert to "date time"
    oss << date_time(system_clock::to_time_t(now));
    oss << '.' << setw(3) << ms.count();
 
    return oss.str();
}
 
int main()
{
    std::cout << stamp() << '\n';
}

用一个有9999个元素的链表测试

Continue版本

耗时接近39秒

上述最优版本

耗时3ms???? 差距着实有点大,有点小迷

image.png

结论

  • 用continue似乎真的耗时会多很多,不仅在LeetCode上是如此,本机测试也是这般(只不过本机测试可能存在误差),但在网络上还没找到特定的资料佐证这一点的,希望有对这方面了解过的小伙伴可以互相交流探讨一下。

注意

初始化前驱为第一个元素!!!!

提前跳出来,从前往后找到位置后就可以跳出来了!!!

虚拟头的巧用

LeetCode上有时给我们的都是不带头结点的链表,我们可以自己构造一个虚拟头结点!

写在最后

  • 关于排序这一章其实还有很多内容,后续也还会更新一下希尔排序,快速排序,归并排序等内容!
    最后发现官方题解耗时平均只需要10ms,给跪了直接。。
posted @ 2021-10-21 13:35  Melo~  阅读(15)  评论(0编辑  收藏  举报