手撕初阶数据结构之---排序

 

1.排序概念及运用

排序:所谓排序,就是使⼀串记录,按照其中的某个或某些关键字的⼤⼩,递增或递减的排列起来的操作。

常见的排序算法

直接插入排序的时间复杂度是O(N^2)

这个是最差的情况下,就是大的在前面,小的在后面

希尔排序就是直接插入排序的优化版本

将时间复杂度优化为O(N^1.3)

时间选择排序是雷打不动的O(N^2)

堆排序是O(N logN)

冒泡排序是O(N^2)

在数组有序的情况下我们能优化成O(N),但是这种情况很少见

 

2.常见排序算法

1.插入排序

1.直接插入排序

当插⼊第 i(i>=1) 个元素时,前⾯的 array[0],array[1],…,array[i-1] 已经排好序,此时⽤ array[i] 的排序码与 array[i-1],array[i-2],… 的排序码顺序进⾏⽐较,找到插⼊位置即将 array[i] 插⼊,原来位置上的元素顺序后移

 

现在我们有一堆的数字,现在我们一个一个进行比较,只要我们发现后面的值比前面的值大,那么我们就把前面的值放到当前的位置,继续往前进行比较,那么这么就会导致前面遍历过的内容都是有序的,将后面乱序的数字一个个插入到前面有序的序列中,使其变得有序

这种思想和打牌是一样的

给出一个数组10 9 8 7 6 5 4 3 2 1

现在我们要变为升序

我们创建变量tmp

先将9存在tmp中,将9与10进行比较,如果9比10小的话,那么就将10挪到9的位置

然后用tmp为第一个位置重新赋值,那么第一个数字就是9

然后我们前两个数字就是有序的数列

然后依次从后面拿数字到前面进行比较

//2 5 8 1 5 6
//直接插入排序
void InsertSort(int* arr, int n)
{
    //i最后一次是n-2,对应数组中倒数第二个数字
    for (int  i = 0; i < n-1; i++)
    {
        //i不能小于n,假设n=10
        //那么i<9
        //当end=8时,那么tmp就是9,那么就存在越界行为了
        int end = i;//end从0开始
        int tmp = arr[end + 1];//有序区间的后一个数字

        while (end>=0)
        {
            //比较
            if (arr[end] > tmp)//end位置的数据大于tmp,那么tmp往前走
            {
                arr[end + 1] = arr[end];//将end位置的数据往后挪
                end--;
            }
            else//end位置的数小于tmp位置的,我们就跳出循环
            {
                break;
            }
        }
        //此时的end就小于0,跳出循环
        arr[end + 1] = tmp;
    }
}

在内层循环中

随着i的变化,交换的情况最差就是1 +2 +3 +(n-1)

那么内层循环的时间复杂度就是O(N)

外层循环的时间复杂度也是O(N)

那么这个直接插入法的时间复杂度就是O(N^2)

数组有序且为降序的情况下时间复杂度最差

最差的情况下时间复杂度为O(N^2)

如果是有序为升序下,那么时间复杂度就是O(N)

数组为降序序列时,直接插入排序能得到优化?

将N个数据划分成很多个段,每个段单独进行直接插入排序

那么这样我们就能减少时间复杂度了

那么我们将直接插入排序我们就得到了希尔排序

2.希尔排序

数组为降序序列时,直接插入排序能得到优化?

将N个数据划分成很多个段,每个段单独进行直接插入排序

那么这样我们就能减少时间复杂度了

那么我们将直接插入排序我们就得到了希尔排序

1.对每一段进行预排序

2.直接插入排序

预排序使数组接近有序

希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。

它是在直接插⼊排序算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。

//希尔排序
//时间复杂度为O(N^1.3)
void ShellSort(int* arr, int n)
{
    int gap = n;
    while (gap>1)//控制gap的变化
    {
        gap =gap/3+1;//保证最后一次gap一定为1
        //当我们的gap为1的时候那么就是直接插入排序
        for (int i = 0; i < n - gap; i ++)//每一组的几个数据之间进行比较
        {
            /*
            如果最后一次的话i为n-2,那么end为n-2
            那么tmp=n-2+gap   那么就会存在越界现象
            所以我们的条件是i<n-gap

            那么end=n-gap-1
            tmp=n-gap-1+gap=n-1
            那么tmp最后一次取到的数据就是最后一个数据
            */
            int end = i;
            int tmp = arr[end + gap];
            while (end >= 0)
            {
                if (arr[end] > tmp)//那么说明tmp小,那么小的就要往前面放
                {
                    arr[end + gap] = arr[end];//我们现将end这个位置的数挪到end+gap这个位置
                    end -= gap;
                }
                else//就说明tmp对应的数大于end对应的数,那么对于跳出循环的代码我们是相当于tmp是不动的
                {
                    break;
                }
            }

            //跳出循环之后我们的tmp应该放在end+gap这个位置
            arr[end + gap] = tmp;

        }
    }

}
/*
9 1 2 5 7 4 8 6 3 5
现在我们的gap为3
那么就是隔三个为一组
那么第一组就是9 5  8  5
因为5小于9
那么先将9放到5的位置,然后end-=gap
因为一开始的end为0,那么end-=gap之后那么end就变为了-3
然后因为end<0那么跳出循环
然后我们end+gap=-3+3=0
那么arr[3]这个位置就放tmp的值
这个位置就是第一个位置
那么就完成了end和end+gap这两个位置的数据的置换


5 1 2 9 7 4 8 6 3 5
我们外循环的条件改为i+=gap
每次递增gap
那么我们的end就到了9的位置,tmp到了8的位置

因为tmp小于end
那么我们就将tmp位置的数覆盖为end的值,tmp位置上面就变成了9了
然后end-=gap
此时的end=3,经过了end-=gap操作之后end=0
我们再拿出tmp中的8和end对应的5进行比较,那么我们就break了循环

那么我们就将tmp里面的8放到end+gap 的位置
那么就是下标为3的位置
5 1 2 8 7 4 9 6 3 5

那么我们再次进入循环
end就走到了9的位置,tmp就走到了end+gap就是最后的5的位置了

因为5比9小,那么先将9覆盖到tmp 的位置
然后end-=gap
那么end就走到了现在8的位置
然后再次进入循环,那么8于tmp中的5进行比较
那么5<8
那么8放到9的位置,9的位置就是end+gap
将8放完之后end-=gap
那么end就回到了0
那么5和5进行比较
那么我们就break出来
然后我们将tmp中存的5放到end+gap这个位置

5 1 2 5 7 4 8 6 3 5 9
我们之前的第一组是9 5 8 5  就是每隔gap个就是一组
那么现在就是5 5 8 9 

那么现在我们控制的是一组
那么我们还要在外面进行一个for循环

我们定义的gap是3
那么就是定义的3组
第一组是9 5 8 5
第二组是1 7 6
第三组是2 4 3
那么第二组换完就是1 6 7
那么第二组换完就是2 3 4 

那么经过这么一趟排序
我们的数组就变成了5 1 2 5 6 3 7 4 9
那么数组又接近有序了

然后gap/3=1
那么就是1个为1组
那么就变成了直接插入排序了 

那么对于希尔排序来说,预排序的条件是gap>1
直接插入排序的话是gap==1
那么最后我们的一次直接插入排序我们就得到了1 2 3 4 5 5 6 7 9


那么通过预排序小的在前,大的在后
那么我们通过直接插入排序的话就可以减少次数

 我们现在从4层循环优化成3层循环

现在我们是
第一次我们先排第一组的前两个数据
因为将之前的for循环删除了
那么现在我i++
那么我们接着排第二组的前两个数据
然后i++
那么我们就接着排第三组的前两个数据

然后我们i++
我们就进行第一组的第三个数据和前两个数据进行比较
*/

希尔排序是三层循环

n/3

n/3/3

……

除到1

log3 n=x

x是次数

那么3^x=n

如果n=9的话,那么x=2

 

假设一组里面的数据是8 5 2

那么我们的交换次数最差是3次

8和5进行交换一次

2和8 和5分别交换一次

那么总共就是三次

当gap=1的时候,数组基本有序,直接插入排序时间复杂度为O(N)

我们推荐在使用希尔排序的时候使用gap=gap/3

如果是gap/2的话那么循环的次数就太多了

2.选择排序

1.直接选择排序

  1. 在元素集合 array[i]--array[n-1] 中选择关键码最⼤(⼩)的数据元素

  2. 若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换

  3. 在剩余的 array[i]--array[n-2](array[i+1]--array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素

//直接选择排序优化版本
//直接选择排序的时间复杂度是O(N ^ 2)
void SelectSort02(int* arr, int n)
{
    int begin=0;//指向第一个数据
    int end = n - 1;//指向最后一个数据
    //遍历整个数组找到最小的数保存到mini中

    while (begin < end)//当begin=end的时候那么这个时候我们就调整完了整个数组了
    {
        //一开始将最大的和最小的都定义在begin这个位置
        int mini = begin, maxi = begin;
        for (int  i = begin+1; i <=end; i++)
        {
            //起始的条件取决于begin,我们每次从begin+1这个位置进行找大的和找小的数据
            if (arr[i] > arr[maxi])
            {
                maxi = i;//重新定义最大的值的下标
            }
            if (arr[i] < arr[mini])
            {
                mini = i;//重新定义最小的值的下标
            }

        }

        //跳出循环之后我们用最小值和begin这个位置的值进行交换
        //最大值和end进行交换
        //优化:避免maxi begin都在同一个位置,begin和mini交换之后,maxi数据变成了最小的数据
        if (maxi == begin)
        {
            //因为此时的maxi和begin指向的是同一个数字
            //那么我们直接为maxi重新赋值
            //我们将maxi的值赋值到mini这个位置
            maxi = mini;
        }
        /*
        9 5 1
        在这个数组中,我们的maxi和begin指向的都是同一个数
        那么就满足了这个条件语句了
        此时的maxi指向的是9
        mini指向的是1
         那么我们将maxi重新进行赋值,复制为mini
         那么此时的maxi和mini同时指向的是1
         我们在下面的交换函数中
         我们先将mini和begin进行交换
         那么就是将1和9进行交换

         那么现在的数字就是
         1 5 9
         那么此时我们就完成了最小值的交换了
         然后进行最大值的交换
         此时的end和maxi都指向的是9
         那么相当于不进行操作
         那么最后我们就得到了我们想要的排序了

        */
        Swap(&arr[mini], &arr[begin]);
        Swap(&arr[maxi], &arr[end]);

        //交换完成之后我们继续往中间进行找大小的操作
        begin++;
        end--;

    }
}
/*
在外层的while循环中,
n  n-2  n-4  n-6   …………2
每次减少首尾两个数据进行比较

那么while循环的时间复杂度就是O(N)

内层的for循环
n-1  n-2  n-3  ………… 1
这里的for循环时间复杂度也是O(N)


那么这个直接选择排序的时间复杂度是O(N^2)
和冒泡排序是一样的
*/

2.堆排序

HeapSort

//堆排序
//期望空间复杂度是O(1) ,不额外的开辟空间
//时间复杂度是O(n*log n)
void HeapSort(int* arr, int n)
{
    //根据数组来建堆,那么我们就需要便利数组
    //建小堆---降序
    // 向上调整算法建堆
    //for (int i = 0; i < n; i++)
    //{
    //  AdjustUp(arr, i);//我们给的参数是i,就是开始 调整数字的 下标,我们是从数组第一个元素进行调整的,i=0开始的
    //}

    //向下调整算法建堆
    //我们需要通过最后一个元素的下标n-1找到他的父节点,从这个点开始进行操作
    //父节点:(n-1  -1)/2
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdhustDown(arr, i, n);//我们从i这个位置开始向下调整
        //i一开始的位置就是最后一个数据的父节点,我们从这个父节点开始向下操作

    }

    //循环将堆顶数据跟最后位置的数据(会变化,每次减小一个数据)进行交换
    int end = n - 1;//n是数组的有效个数,那么最后一个数据的下标是n-1
    while (end > 0)//end会一直--,但是end必须大于0
    {
        Swap(&arr[0], &arr[end]);//将第一个数据和最后一个数据进行交换
        //那么我们就要对现在的剩下的堆进行调整
        //我们采用向下调整的方法
        AdhustDown(arr, 0, end);//我们从根进行调整,那么第二个参数就是0
        //n-1=end,就是我们只需要对剩下的n-1个数据进行向下调整,那么第三个参数就是我们调整的有效数据n-1个
        end--;
    }
}

堆排序使用时需要的向下调整算法AdhustDown以及交换函数Swap

//向下调整
void AdhustDown(int* arr, int parent, int n)
//第一个数据是我们要调整的数组,第二个参数是父节点,就是我们开始进行调整的位置的下标对应的元素,第三个是数组中有效的元素个数
{
    //我们已知Parent,那么我们能够通过2i+1或者2i+2找到这个父节点的左右子节点
    int child = parent * 2 + 1;//左孩子
    while (child < n)//我们不能让child因为++操作导致越界
    {
        //找左右孩子中最小的那个,我们与父节点进行交换操作
        if (child + 1 < n && arr[child] > arr[child + 1])
        {
            //假设我们的子节点只有一个左节点的话,那么我们就可以通过child+1<n      
            //这个条件避免child++,避免了越界访问,我们直接让左节点和父节点进行交换
                    //child+1必须小于n我们才能往下进行调整操作,避免越界访问
                    //假设左边的是56,右边的是15
            child++;//我们进行child++操作,然后child就跑到了15的位置,然后我们将15和父节点进行交换
        }
        if (arr[child] < arr[parent])//如果子节点小于父节点的话我们就进行交换
        {
            Swap(&arr[child], &arr[parent]);
            //交换完成之后我们让parent走到child的位置
            parent = child;
            //child走到当前位置的左孩子的位置
            child = parent * 2 + 1;
        }
        else//如果Parent比child小的话,那么我们就没必要向下进行调整了
        {
            break;//我们直接跳出
        }
    }
}
//交换函数
void Swap(int* x, int* y)//一定要传地址才能进行交换了
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

3.交换排序

1.冒泡排序

//冒泡排序
//时间复杂度为O(N^2)
void BubbleSort(int* arr, int n)//数组以及数组中的有效数据个数
{
    for (int i = 0; i < n; i++)
    {
        int exchange = 0;
        for (int j = 0; j < n - i - 1; j++)
        {
            //<就是降序
            if (arr[j] < arr[j + 1])//大的在后面,那么我们就进行交换
            {
                exchange = 1;
                Swap(&arr[j], &arr[j + 1]);
            }

        }
        if (exchange == 0)
        {
            //那么就说明我们经历了一趟内循环,我们的exchange并没有改变,就说明这个数组可能之前就是有序的
            break;
        }
    }
}

2.快速排序

Hoare版本

 快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素

序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩

于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列

在相应位置上为⽌。

简单意思就是说:我们在数组中去一个数当作基准值,然后,整个数组被分为两个部分

左边的一个部分小于基准值,右边的部分大于基准值

那么这就相当于一个二叉树了

基准值是根节点,左边的是左子树,右边的是右子树

然后重复上面的操作,将左序列和右序列单独取出来,重复上面的操作

数组是左和右这个区间,在这个区间之中我们要找基准值mid

那么我们的左子序列的递归的右边界是mid-1

右子序列:mid+1,right

如何找基准值呢?

我们先将数组中第一个数据定义为基准值

right:从右到左找基准值小的数据

left:从左到右找比基准值大的数据

6 1 2 7 9 3

一开始的基准值Keyi指向的是6

left指向的是1

right指向的是3

那么我们先进行right的行动,找到了3

然后left往右找到了7

然后将两个指针指向的值进行交换

那么我们得到了

6 1 2 3 9 7

然后left指向的是3,right指向的是7

交换完成之后再重复刚才的过程

right左走找到3

left右左找到9

那么right就到了left的左边了

这个时候我们将keyi指向的6和right指向的3进行交换

那么我们得到了3 1 2 6 9 7

此时我们的基准值 在right的位置

right指向的位置的左边都比基准值小,右侧都比基准值大

那么对于左序列的话3 1 2

我们找基准值

right一开始就找到了比基准值小的数,但是left一直++,直到比right大了

然后我们就将right指向的数据和keyi指向的数据进行交换

2 1 3

那么此时的right是基准值

我们再对当前序列进行划分

左子序列就是2 1,右序列为空

我们再对左序列进行判断

我们的right指向的1

left指向的是1

keyi指向的是2

此时的right和left是重合的,那么就不影响我们找基准值,相等的话我们继续进行循环

那么right刚好找到的就是1比2小

right找好了

接下来找left,left++,越界了,已经比right大了,那么我们就跳出循环,

那么我们就将key和right指向的值进行交换

那么最后就得到了

1 2

那么此时我们的左序列就剩一个序列了

right和Left都是空了,就是left和right相等了

那么我们就没必要找基准值了

那么对于这个数组的话,我们的左子序列已经递归完了

那么我们还要递归右子序列

那么我们通过快排不断地找基准值,根据基准值划分左右区间,一直进行递归操作

我们要创建log n个函数栈帧

那么空间复杂度就是O(log n)

一层递归循环的时间复杂度是O(N)

但是我们要递归logN层

时间复杂度是O(N logN)

//块排子方法
//right:从右到左找基准值小的数据

//left:从左到右找比基准值大的数据

//hoare版本找基准值
int _QuickSort(int* arr, int left, int right)//找基准值,返回Int
{
    int keyi = left;//一开始的left是最左边的,那么我们就先临时将第一个数据定义为基准值
    //我们从left+1,right这个范围进行寻找
    left++;
    //我们要让right走到left的左边
    while (left<=right)//一定要=,
        //left和right相遇的位置的值比基准值大
    {
        while (left<=right&& arr[right]>arr[keyi])//对应的值大于基准值
        {
            right--;//从右往左换写一个数字进行寻找
        }
        //走到这里说明right找到了比基准值小/等于?
        while (left <= right && arr[left] < arr[keyi])
        {
            left++;//还没有找到比基准值大的数据,那么我们就往右接着找
        }
        //说明我们的right和left都找好了
        if (left <= right)//交换的前提是left<=right
        {
            //那么我们将这两个下标对应的数据进行交换操作
            Swap(&arr[left++], &arr[right--]);
        }

    }
    //只要lefy大于right了,那么我们就跳出循环走到这里了
    // 将right指向的值和key指向的值进行交换
    //跳出循环,那么就是right走到了left的左边,那么我们将right指向的值和keyi指向的值进行交换
    Swap(&arr[keyi], &arr[right]);
    //那么交换完成之后,那么right指向的数据就是我们的基准值了
    return right;
}

//快速排序
void QuickSort(int* arr, int left, int right)//左右位置的下标
{
    if (left >= right)//left走到了right的右边或者重叠(只有一个数据的话)
    {
        return;
    }
    //找基准值
    int keyi = _QuickSort(arr, left, right);

    //左子序列:[left,keyi-1]
    QuickSort(arr, left, keyi - 1);
    //右子序列:[keyi+1,right]
    QuickSort(arr, keyi +1 ,right);

}

挖坑法

思路:

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

不断地挖坑不断地填坑

一开始我们是不知道坑在哪里的,那么我们就随便定义一个坑

6 1 2 7 9 3 4 5 10 8

我们将6定为坑hole

我们再定义一个基准值key=6指向坑所指向的位置

一开始的left指向6,right指向8

先让right往左走找比基准值小的

找到了5

那么我们直接将5拿挖走去填我们之前定义的坑

那么此时的right的指向的位置就是一个坑

那么数组就变成这样了

5 1 2 7 9 3 4 hole 10 8

接下来left从左往右找比基准值大的

找到了7

那么我们将7挖出来去填坑

那么left这个位置就是一个新的坑

5 1 2 hole 9 3 4 7 10 8

然后right再找小

找到了4

那么我们将4挖出去填坑,那么此时的right成为新的坑

5 1 2 4 9 3 hole 7 10 8

左边继续找大,找到了9

将9挖出来,去填坑

那么此时的left的位置就是坑

5 1 2 4 hloe 3 9 7 10 8

然后right继续找小

找到了3

把3拿去填坑

那么此时的right的位置就是坑

5 1 2 4 3 hole 9 7 10 8

然后left接着走

然后left和right已经重合了

那么我们将之前的基准值填到坑里面

5 1 2 4 3 6 9 7 10 8

那么基准值6的左边比基准值小,右边比基准制度大

//挖坑版
int _QuickSort2(int* arr, int left, int right)//找基准值,返回Int
{
    int hole = left;
    int key = arr[hole];
    while (left<right)//只要left和right重合那么我们就跳出循环
    {
        while (left < right && arr[right] > key)//没有找到比基准值小的,后面的条件我们就不用加等号了,因为可能存在这个数组的元素都是一样的情况
        {
            right -- ;
        }
        //那么这里我们找到了小的,那么我们就将这个数字拿去填坑,那么此时right指向的位置就是新的坑
        arr[hole] = arr[right];
        hole = right;
        while (left < right && arr[left] < key)//没有找到比基准值小的
        {
            left++;
        }
        //走到这里就说明找到了要拿去填坑的数字
        arr[hole] = arr[left];
        hole = left;
    }
    //走到这里说明我们的left和right可能重合了
    //那么我们将基准值放到坑位
    arr[hole] = key;
    return hole;//将坑位返回,此时的坑位就是基准值
}
lomuto前后指针

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

定义两个变量prev和cur

cur指向位置的数据跟key值比较,若arr[cur]<arr[keyi]

那么prev向后走一步并和cur交换数据

若arr[cur]≥arr[keyi]

那么cur继续往后走

直到cur越界,那么我们将此时的prev和key的值进行交换

那么此时prev指向的位置就是我们要的基准值

//lomuto前后指针法
int _QuickSort3(int* arr, int left, int right)//找基准值,返回Int
{
    int prev = left, cur = left + 1;
    int keyi = left;
    while (cur<=right)//当我们的cur越界了我们就不进行循环
    {
        if (arr[cur] < arr[keyi]&&++prev!=cur)//两个指针相等的话,那么我们就不进行交换
        {
            //只要cur找到小的,就让prve++然后进行交换
            Swap(&arr[cur], &arr[prev]);
        }
        cur++;
    }
    //cur越界了
    //将之前定义的基准值和现在prev指向的值进行交换
    Swap(&arr[keyi], &arr[prev]);
    return prev;

}

快排的非递归版本

上面介绍到的都是递归版本的

那么我们接下来就介绍下快排的非递归版本

那么非递归的快速排序的需要借助数据结构:栈

我们将区间right和left依次入栈

那么我们再依次取出来,得到的就是0和5

假设我们经过依次寻找,那么这里的基准值是

那么我们就能得到左区间和右区间

左区间[left,keyi-1]

右区间[keti+1,right]

总结下来我们就是先将区间入栈

先是right再试left

我们再出一次栈

那么我们根据这个区间找基准值

前面介绍的三种方法

我们是right先入栈,left先出栈,那么取出来的时候就是left先出栈,right再出栈

那么第一次的话我们的左区间是[0,2],右区间是[4,5]

我们先将右区间入栈,再将左区间入栈

那么我们进行出栈操作,取到的是left=0,right=2

那么在这个区间我们的keyi=0,这里的是下标

那么我们再重复上面的左右区间的方法继续找基准值

左区间[left,keyi-1] [0,-1]

右区间[keti+1,right] [1,2]

这里的基准值是不需要继续去找基准值的,因为这里的keyi-1已经小于0了

那么我们就不用入栈了

就是非有效区间不入栈

但是右区间是有效的区间,那么我们进行入栈操作

那么我们接着对右区间进行递归操作

我们将left和right取出来

left=1,right=2

我们对这个右区间找基准值

那么我们这里的基准值keyi=1,就是数组中对应的3

那么根据当前的基准值我们又能划分区间了

左区间[left,keyi-1] [1,0] 非有效区间不进行入栈

右区间[keti+1,right] [2,2] 只有一个数据不进行入栈

那么此时我们的左区间所有的内容都递归完了

这里其实是循环,不是递归,我们循环取栈的数据模拟的递归

我们排右区间

我们将栈中的left和right取出

left=4,right=5 我们找到的基准值keyi=4,指向6这个数字

那么我们再次根据这个基准值划分左右区间

左区间[left,keyi-1] [4,3]非有效区间,不入栈

右区间[keti+1,right] [5,5] 只有一个有效数据,不如栈

那么我们的右区间就已经找好基准值了

那么这个数组的右区间已经递归完了

通过栈来实现快速排序

//非递归版本的快速排序
//借助数据结构---栈
void QuickSortNonR(int* arr, int left, int right)
{
    ST st;
    STInit(&st);//栈的初始化
    //先将右区间入栈,再就是左区间
    SrackPush(&st, right);
    SrackPush(&st, left);

    //栈只要不为空,那么我们就取栈顶元素
    while (!StackEmpty(&st))//取两次,作为左右区间
    {
        //取栈顶元素--两次
        //那么第一次取到的就是left,第二次取到的就是right
        int begin = StackTop(&st);
        SrackPop(&st);//取完栈顶元素接着进行出栈操作

        //取出来的左右区间
        int end= StackTop(&st);
        SrackPop(&st);

        //利用取出来的左右区间创建新的区间
        //[begin,end]---找基准值
        //双指针找基准值
        int prev = begin;
        int cur = begin + 1;
        int keyi = begin;
        while (cur<=end)//当我们的cur越界了我们就不进行循环
        {
            //要确保prev++完成之后的值不等于cur我们才能进入循环进行操作
            if (arr[cur] <= arr[keyi]&&++prev!=cur)
            {
                Swap(&arr[cur], &arr[prev]);
            }
            cur++;
        }
        //跳出循环的时候说明cur已经越界了
        Swap(&arr[keyi], &arr[prev]);
        //那么此时的prev指向的位置就是基准值的位置
        keyi = prev;
        //解下来我们根据这个基准值再对左右区间进行操作

        //根据基准值划分左右区间
        //左区间:[begin,keyi-1]
        //右区间:[keyi+1,end]

        //如果我们根据这个begin和end找基准值的话那么这两个量是不会越界的

        //我们是需要对下面的keyi+1和keyi-1进行判断是不是有效的值


        if(keyi + 1<end)//等于end的话就是说明有效数据只有一个
        //如果是大于的话,那么就是越界情况了,比end还大
        //如果满足这个条件的话那么我们就入栈
        {
            //先入右区间,
            SrackPush(&st, end);//右区间内的右区间
            SrackPush(&st, keyi + 1);//右区间内的左区间
        }

        if (keyi - 1>begin)//如果是[0,-1]就不是一个有效的区间
        {
            //再入左左区间
            SrackPush(&st, keyi - 1);//左区间内的右区间
            SrackPush(&st, begin);//左区间内的左区间
        }


    }

    STDestory(&st);//栈的销毁
}
/*
一开始我们传的left是0,right是5
我们进行入栈操作
先入的是5,再入的是0

入完栈之后,判断栈是否为空
不为空的话就进入循环取栈顶元素
先取0,再取5
那么0和5组成了一个空间
那么我们对这个区间找基准值keyi
第一次的基准值我们找的是3,这里是下标
那么我们使用双指针法进行基准值的寻找
找到基准值之后我们根据这个基准值进行新区间的划分
划分左右区间
因为keyi=3
那么左区间就是[0,2] 右区间是[4,5]
那么这两个区间都是有效区间,那么就都进入了if条件语句中
那么进行条件语句中将左右区间进行入栈的操作
先入的是右区间
再就是左区间
先入的是5 4
再就是 2 0
那么现在这个栈从上到下看就是0 2 4 5
因为此时的栈不是空的,所以我们还继续循环
然后我们将栈顶元素取出来[0,2]
我们再根据这个区间进行基准值的寻找
那么这个区间的基准值是0
那么对于[0,2]这个区间的话我们分出的区间是[0,-1]和[1,2]
那么我们对这段区间进行判断是否为有效的区间
对于[1,2]满足条件,那么就入栈
但是[0,-1]这个不满足,那么就不入栈
那么此时的栈中我们入的是 2 1 
那么此时的栈中元素从上到下看就是2 1 4 5

然后因为栈不为空,那么继续进行循环
将2 1 取出找基准值
那么对于 [1,2]这个区间来说的话
我们又分出两个区间
[1,0]和[2,2]
两个区间都不是有效的区间
那么就都不入栈


那么我们就进行4和5的出栈了
[4,5]
我们对[4,5]找基准值
基准值是4
那么分出的区间都是无效区间
那么此时的栈就是空的
那么我们对于每个区间我们都找好了基准值了
那么我们就退出了循环
*/

4.归并排序

归并排序算法思想:

归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divideand Conquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。 归并排序核⼼步骤:

对于图中这8个数据,我们实现二分,从中间一分为二

如果我们单看分解的那部分,这个图长得很像二叉树

那么我们可以参考二叉树递归的过程对他进行分解

分解到只有一个数据的时候

合并的时候两两一合并

两个有序的数组合并

8个有序数组两两一合并成为4个数组

4个有序数组两两已合并成为2个数组

//归并排序

void _MergeSort(int* arr, int left, int right,int*tmp)//左下标和右下标求中间值,还要将开辟的空间的指针进行接收,进行合并数据的存储
{
    //递归结束的条件,就是刚好被分的数组中只剩下一个元素,那么我们就没必要往下进行递归了
    if (left >= right)
    {
        return;
    }
    //我们需要根据left和right不断进行中间下标的寻找
    int mid = (left + right) / 2;

    //根据mid得到两个区间,不断地进行二分,将数据分成两组
    //[left,mid]  [mid+1,right]
    _MergeSort(arr, left, mid, tmp);//左区间
    _MergeSort(arr, mid + 1, right, tmp);//右区间


    //能够走到这里说明我们已经分解完了,那么我们就开始进行合并的操作了
    //[left,mid]  [mid+1,right]
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int index = begin1;

    //比较两个序列中的数据的大小
    //序列中可能是一个数字,也可能是4个数字
    while (begin1 <= end1 && begin2 <= end2)//那么此时的这些下标都是有效的
    {
        if (arr[begin1] < arr[begin2])
        {
            tmp[index++] = arr[begin1++];
        }
        else//说明begin2小
        {
            tmp[index++] = arr[begin2++];

        }
    }
    //要么begin1越界,要么begin2越界
    while (begin1<=end1)//begin2越界,那么我们就将begin剩下的元素安排到数组中
    {
        tmp[index++] = arr[begin1++];
    }
    while (begin2 <= end2)
    {
        tmp[index++] = arr[begin2++];
    }


    //[left,mid]  [mid+1,right]
    //把tmp中的数据拷贝回arr中
    for (int i = left; i < right; i++)
    {
        arr[i] = tmp[i];
    }



}

void MergeSort(int* arr, int n)
{
    //开辟一个和原数组大小一样的数组用于存储合并后的数组
    //动态开辟n个整型空间
    int* tmp = (int*)malloc(sizeof(int) * n);

    _MergeSort(arr, 0,n - 1,tmp);

    free(tmp);
    tmp = NULL;
}
/*
对于这个归并排序的子函数进行解释
在第一步分解中
假如我们传的left=0,right=7
数组原先的数据是这样的
10 6 7 1 3 9 4 2
那么我们根据左右下标将mid求出
为3
那么我们就分出了两个区间
[0,3]   [4,7]
然后我们下面又对着两个区间进行递归
那么我们最后就实现了将这个数组分解的效果了

那么我们将这么一个完整的数组分成多个单独的序列
10  6  7  1  3  9  4  2  
那么就是这8个单独的序列
我们两两合并
一开始的begin1和end1都是0
begin2和end2都是1
那么我们先在第一个while循环中间进行比较并进行赋值到tmp中
因为10>6那么我们进入到else中
那么tmp[index++] = arr[begin2++];
将begin2的值赋值到tmp中第一个位置
然后begin++,index++

但是现在我们只是将6放到tmp中,10还没有放
那么我们在后面的while就进行了处理了
因为我们的begin1此时已经比end1大了,就是越界了
那么我们就进入到第一个条件语句中
tmp[index++] = arr[begin1++];
此时的index就是指向的tmp数组中的第二个位置了
那么我们的第二个位置就复制成之前的begin1
那么第二个就是10了
然后begin1++

那么总结:
在分解为一个个的元素之前,我们的10和6是一个区间的
这个区间求中间值分左右区间,合并完成之后我们完成另一个区间的合并




我们在逐渐的合并过程中,这个begin1  begin2 end1  end2
都在变化,   当分解之后的第一个合并的时候,begin1和end1都是0 
begin2和end2都是1     那么将6和10合并之后  
我们就回到了上一级 上一级的左区间已经合并完成,
但是右区间没有合并,那么我们按照6和10的步骤合并1和7    
合并完成之后返回上一级,那么我们就进行现有的(6 10)( 1  7 )
这两个区间的合并    此时的begin1是0,end1是1     begin2是2
end2是3   那么我们逐次进行比较并为tmp中进行赋值    
那么最后我们就合并出了    1 6 7 10   那么我们会到上一级   
上一级的左区间合并完成,现在完成右区间的合并后,合并之后就是 
2 3 4 9   那么最后这两个区间合并之后就是1 2 3 4 6 7 9 10


这个归并排序可以想成二叉树,从叶子到根,从最下面合并到最上面
*/

可以想成一个二叉树,我们分解之后进行合并的操作我们可以想象从下面到上面逐次合并

空间复杂度:logn 根据层数来定的

时间复杂度:在归并的子函数中的第一个while循环中就是相当于n个数进行比较

那么这个循环的时间复杂度是O(N)

那么第二个循环的话,进行完第一个循环之后应该是剩下不了几个数据的

那么最大的情况就是n/2

下面的for循环拷贝数组中的值,那么时间复杂度也是n

那么单次递归时间复杂度就是O*(N)

但是递归的深度是logN

那么这里的时间复杂度就是O(N log n)

那么时间效率和快排是一样的

5.非比较排序

计数排序

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

我们需要先找到数组中的最大值,然后将数据中的个数变为下标

根据数组中最大值来开辟对应的空间

新数组的大小如何确定?

原数组中最大的值来确定(如果数组中存在负数的话是不行的)(未考虑到最小值)

将负数变为正数,取绝对值是否可行?

正负数绝对值相同的情况下是无法区分正数和负数的

假如我们的给到的值很大,既然我们要用这个当做数组的下标

那么我们是不是要开创这么大的数组呢

其实没有必要的

最大的值-最小的值+1就是这个数组的大小了

举例子:100 101 109 105 101 105

我们数组中的每个元素减去这个数组的最小值,那么不就是这个新数组的下标吗

100-100=0 那么100对应的下标就是0 出现的次数就是1 那么下标为0对应的数据就是1

100对应的下标就是0,下标对应的数据就是100出现的次数

那么其他的也一样

这个计数排序的原理其实就是通过计数

在新的数组中将这个以这个数据出现的次数为元素

我们依次进行打印

比如说在上面那个代码里面

100出现了1次,那么我们打印1次

101出现两次,那么我们打印两次

那么这样我们就达到了排序的目的了

最大值-最小值就是我们要开辟的空间大小

但是这个适用于数组中存在负数的情况吗?

-5 -5 9 5 1

那么这个数组的大小就是9-(-5)+1=15

那么我们就申请下标为15的空间

下标就是从0到14

那么我们应该将这些数据怎么放?

就是这个数据减去这个数组中最小的值

-5的对应的下标就是-5-(-5)=0

那么这个0对应的数据就是-5出现的次数2

然后放9 9-(-5)=14

那么9对应的下标就是14,那么14对应的就是9出现的次数1

那么我们通过这种思想的话那么空间就不会过多浪费

通过测试,这个计数排序是现有的排序算法中最牛逼的

单位ms

[InsertSort:663 ShellSort:9 SelectSort:4300 HeapSort:5 QuickSort:6 MergeSort:5 BubbleSort:12451 CountSort:1](InsertSort:663 ShellSort:9 SelectSort:4300 HeapSort:5 QuickSort:6 MergeSort:5 BubbleSort:12451 CountSort:1)

//计数排序
void CountSort(int* arr, int n)
{
    //我们需要一个count数组来保存我们每个元素的出现次数
    //我们先需要直到数组中最大值和最小值,在这之后我们才方便数组的开辟

    //根据最大值最小值确定数组大小
    int max = arr[0], min = arr[0];
    //通过遍历找最小值
    for (int  i = 0; i < n; i++)
    {
        if (arr[i] > max)
        {
            max = arr[i];
        }
        if (arr[i] < min)
        {
            min = arr[i];
        }
    }
    int range = max - min + 1;//数组元素的个数
    //申请range个整型大小的空间存放每个数据出现的次数
    int* count = (int*)malloc(sizeof(int) * range);

    if (count == NULL)//开辟失败的话我们就报个错
    {
        perror("malloc fail!");
        exit(1);
    }
    //初始化,让count数组中现在全是0
    memset(count, 0, range * sizeof(int));
    //第一个参数是要被填充的数组的指针
    //第二个参数是要被填充的数据
    //第三个参数就是填充的字节的大小
    //我们的想法是将这个新数组中现在存储的数据都是0、
    //那么我们要填充的字节大小就是rang*sizeof(int)

    //现在统计数组中每个数据出现的次数,然后将次数放到对应的下标

    for (int  i = 0; i < n; i++)
    {
        count[arr[i] - min]++;
        //这个arr[i]-min就是我们我们数据映照在新数组的下标,
        //后面的++就能将count这个数对应的下标进行计数
        //就是这个数据的出现次数通过这个代码就能存进新数组中
    }

    //取count中的数据往arr中放
    //我们现在遍历的是count,数组大小是range
    int index = 0;
    for (int  i = 0; i < range; i++)
    {
        while (count[i]--)
        {
            arr[index++] = i + min;
        }
    }
    /*

    100   101   102   105   101   100

    2   2   1   0   0   1     数据出现的次数
    0   1   2   3   4   5


    在上面的for循环中,当i=0时,count[i]=2
    那么arr[0]=100+0
    这里是内循环两次的,因为循环条件是count[i]--
    那么第二次内循环就是arr[1]=100
    那么原数组中的前两个数据我们就打印好了

    当i=1时,我们内循环的次数就是这个数出现的次数
    count[i]=2
    那么我们就内循环两次
    那么我们就将下标为2和3的位置都赋值上101了
    、
    那么最后我们的arr中的数据就是这样的
    100 100 101 101 105
    */




}

空间复杂度是O(range)

我们在第一个循环中的时间复杂度是O(N)

第二个循环是外层循环是O(range)

内层循环的循环次数还是取决于实际的数据n

那么这里的这个循环的时间复杂度是range+n

那么总的时间复杂度就是O(range+n)

那么我们总结一下计数排序的特性:

计数排序在数据范围集中时,效率很高,但是使用范围及场景有限

只适用于整数排序,不能用小数排序

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

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

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

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

6 2 4 9 1 2

1 2 2 4 6 9

对于原先数组的第一个2的话,如果在排序之后这个2还是第一个二的话,那么这两个二的相对次序保持不变

相对次数保持不变的话,那么我们就称这个排序为稳定排序

相反,相对次数发生变化的话,那么这个排序就是不稳定的排序

直接选择排序是不稳定的

通过找大找小,交换的过程中就会导致相对次序的变化

直接插入排序就是将后面待排序的数据一个一个往前面插入,和打扑克牌一样的

这里是不存在相对位置发生改变的

那么直接插入排序是稳定的

希尔排序我们是会对数据进行分组的,每个组进行独立的排序,就可能导致原本在后面的数字排在前面

那么次序就会发生变化,那么希尔排序是不稳定的

举例:5 8 2 5 9

gap=2

那么5 2 9 是一组,8和5一组

我们先排第一组、

排完了就是2 8 5 5 9

再排第二组

2 5 5 8 9

可以发现之前在后面的5跑到前面去了,那么相对次序就发生了改变

堆排序的思想:将堆顶的数据和堆尾的数据进行交换

那么这里的相对位置就发生了交换了

归并排序是从中间位置划分,两两一排序,相对位置是不会发生变化的

快排:在找基准值的过程中会导致数据发生次序的变化,那么快排就是不稳定的

回忆:栈的底层是数组

队列是链表实现的

 

评论 98
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2301_80863610

感谢啦

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值