DSA3

接口

在这里插入图片描述
在这里插入图片描述
比如说实现栈,会有很多方法,ADT就像说明书一样,或者说像哈姆雷特一样,一百个人眼中有一百个,不同的人实现同一种ADT的方式会不一样,但是ADT规定的接口都要有,这些接口帮助我们实现基本的操作。

vector

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
search的返回值后面会看到为什么这样。在这里插入图片描述

这个应该是个例子而已,_elem和_size都是private的,不能这样直接访问的。得需要类似于begin()和size()这样的函数返回。
在这里插入图片描述
向量的一大特点就是可以扩容。

扩容

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

递增扩容

在这里插入图片描述

加倍扩容

在这里插入图片描述
在这里插入图片描述
紫色为加倍的扩容,递增的扩容显然比加倍多,这是因为指数增长速度快于直线,最终会超过直线。
递增的策略的空间复杂度比加倍的小,加倍策略空间利用率大于等于0.5。递增的利用率会越来越大,一开始为1/INCRECEMENT。

分摊复杂度

平均分析vs分摊分析
平均复杂度或期望复杂度( average/ expected complexity)
根据数据结构各种操作出现概率的分布,将对应的成本加权平均
各种可能的操作,作为独立事件分别考查
割裂了操作之间的相关性和连贯性
往往不能准确地评判数据结构和算法的真实性能
☆分摊复杂度( amortized complexity)
对数据结构连续地实施足够多次操作,所需总体成本分摊至单次操作
从实际可行的角度,对一系列操作做整体的考量
更加忠实地刻画了可能出现的操作序列
可以更为精准地评判数据结构和算法的真实性能

个人理解:平均复杂度是你去统计周围程序员用到向量长度的分布,然后去分析。这个是静态的分析方法。等于是大量的独立实验(不同的程序员)来求出整个地球上(或者说周围的人)用某种扩容方法消耗的平均时间。
分摊复杂度是动态的,是一个程序员的连续实验。

无序向量

有些向量无法排序,除非你自己定义排序方式。还有些向量可以排序,但是整体不是升序或者降序的。
在这里插入图片描述
在这里插入图片描述

注意右移是从后往前开始的。在这里插入图片描述
注意移动的时候要从左往右移动。shrink不是必须的。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

有序向量

在这里插入图片描述
在这里插入图片描述
这样显然是很低效的,光删除就有O(n^2)了。在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
j每次必然+1,所以为O(n)。

二分查找

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
显然直接返回mi不符合上面的语义。这个只是在讲原理。在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个递推式也可以理解为n的二进制展开的位数为logn,每次除以二就是右移一位,那么右移最多logn次。在这里插入图片描述

斐波那契查找

在这里插入图片描述
这样的查找是最优的。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
这种递推式是计算复杂度的重要方式。

二分法的改进

在这里插入图片描述
在这里插入图片描述

这种改进牺牲了直接命中的情况。
在这里插入图片描述
继续改进:
在这里插入图片描述
在这里插入图片描述

这种方法的单调性显然,而不变性需要数学归纳法来推导。
当e<A[mi]时,令hi=mi,mi包含中点的右边最小的元素,它都大于e,可以肯定现在的hi-n都大于e。
而如果A[mi]<=e,那么lo=mi+1。包含mi的左边的元素最大的就是A[mi],它是小于等于e的,那么显然[0,lo)注意右开的元素肯定小于等于e。
当最后的区间长度为0的时候不变性依然保持,那么返回lo–说正确的,此时

-------------------------------lo(hi)-----------------------
0 <=e | >e n
lo-1就是不大于e的最大秩的元素。

排序

在这里插入图片描述
在这里插入图片描述
改进的方向就是如果上一趟为整体有序的,那么就直接可以结束了。
在这里插入图片描述
在这里插入图片描述

如果是绿色部分未排序而红色的部分已经排序,那么如果还用上面的冒泡算法是效率很低的,因为有大量无意义的比较会重复。虽然这样的复杂度依然会低于O(n^2),因为现在是按照梯形计算了。
O(1/2r(n+r))=O(nr)
如果可以直接记录下后面排序好的个数,那么问题直接缩减为O(r^2)+O(n)
而r<n。
在这里插入图片描述
这个复杂度为各个梯形的和。在这里插入图片描述
如果改为>=,就不再稳定了。冒泡排序的最坏情况是O(n^2)。
如果是最原始的冒泡排序,平均复杂度也为O(n^2)。最好为O(n)。
改进的冒泡排序的平均复杂度就不是很好计算了。
基于比较的排序算法的最坏复杂度下界是O(nlogn)在这里插入图片描述

关于这个下界的证明:https://blog.csdn.net/u012745772/article/details/17467869

这个有个信息论的理解很重要,在已知区间,肯定是均匀分布的信息熵最大,那么,如果回答是只有是否,那么二分信息熵最大,如果可以有三个答案,那么三分最大。
最好复杂度的下界为O(n)。至少得比较一轮。

归并排序

在这里插入图片描述
T(n)=2T(n/2)+n=4T(n/4)+2n+…n/2*T(2)+log(n/2)*n
T(1)=1
则T(n)=O(nlogn)。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
注意跳出循环的条件是两个都越界。
这里分析为何C不新开辟空间,如果可能覆盖才需要开辟。如果B排在A前半部分没问题,如果有m个A元素排在前半,p个C的元素排在前半,也不碍事,因为C的前p个已经可以被覆盖了,始终都是不会发生赋值给A之前C的元素就被修改。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意这里计算的是合并的复杂度为O(n)。
参考https://www.runoob.com/w3cnote/sort-algorithm-summary.html

选择排序

选择排序(SelctionSort)
基本思想:
在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
。。。
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。

过程:
在这里插入图片描述

public static void select_sort(int array[],int lenth){

for(int i=0;i<lenth-1;i++){

   int minIndex = i;
   for(int j=i+1;j<lenth;j++){
      if(array[j]<array[minIndex]){
          minIndex = j;
      }
   }
   if(minIndex != i){
       int temp = array[i];
       array[i] = array[minIndex];
       array[minIndex] = temp;
   }

}
}
平均时间复杂度:O(n2)。
最坏情况比较次数为等差数列,和为O(n^2)。但它的交换次数要比冒泡排序小很多。

插入排序

基本思想:
在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

过程:

在这里插入图片描述

相同的场景在这里插入图片描述

平均时间复杂度:O(n2)
public static void insert_sort(int array[],int lenth){

int temp;

for(int i=0;i<lenth-1;i++){
for(int j=i+1;j>0;j–){
if(array[j] < array[j-1]){
temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
}else{ //不需要交换
break;
}
}
}
}
1+2+3+…n-1=O(n^2)。
我倒是感觉插入的位置可以用二分查找,这样的效率会高些。
log1+log2+…log(n-1)=O(nlogn)。

希尔排序

也叫缩小增量排序。
前言:
数据序列1: 13-17-20-42-28 利用插入排序,13-17-20-28-42. Number of swap:1;
数据序列2: 13-17-20-42-14 利用插入排序,13-14-17-20-42. Number of swap:3;
如果数据序列基本有序,使用插入排序会更加高效。

基本思想:
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。

过程:在这里插入图片描述

先按照增量分组,依次进行插入排序。然后缩小增量在进行插入排序。
public static void shell_sort(int array[],int lenth){

int temp = 0;
int incre = lenth;

while(true){
incre = incre/2;

   for(int k = 0;k<incre;k++){    //根据增量分为若干子序列

       for(int i=k+incre;i<lenth;i+=incre){

           for(int j=i;j>k;j-=incre){
               if(array[j]<array[j-incre]){
                   temp = array[j-incre];
                   array[j-incre] = array[j];
                   array[j] = temp;
               }else{
                   break;
               }
           }
       }
   }

   if(incre == 1){
       break;
   }

}
}
可以证明希尔排序的最坏复杂度可以到O(n^1.5),这个幂次和增量选择有关,最好是O(nlogn)。

快速排序

对冒泡排序的改进。
基本思想:(分治)

先从数列中取出一个数作为key值;
将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
对左右两个小数列重复第二步,直至各区间只有1个数。
辅助理解:挖坑填数
在这里插入图片描述
这个算法的不变性是right右边的数是大于等于基准的,因为如果小于肯定会被交换掉,而left左边的也是一定小于等于基准的,否则应该会被换掉。
如何保证right右边的元素大于等于基准,因为是把一个大于基准的数填到right,那么可以保证的。left类似。
单调性很显然,于是正确性可以保证。

平均时间复杂度:O(N*logN)
public static void quickSort(int a[],int l,int r){
if(l>=r)
return;

 int i = l; int j = r; int key = a[l];//选择第一个数为key

 while(i<j){

     while(i<j && a[j]>=key)//从右向左找第一个小于key的值
         j--;
     if(i<j){
         a[i] = a[j];
         i++;
     }

     while(i<j && a[i]<key)//从左向右找第一个大于key的值
         i++;

     if(i<j){
         a[j] = a[i];
         j--;
     }
 }
 //i == j
 a[i] = key;
 quickSort(a, l, i-1);//递归调用
 quickSort(a, i+1, r);//递归调用

}
key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。
最坏情况还是O(n^2),但是最好情况和平均都为O(nlogn)。

关于复杂度证明:参考https://blog.csdn.net/ltyqljhwcm/article/details/52145746
https://blog.csdn.net/i96jie/article/details/84256915

堆排序(HeapSort)

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
  在这里插入图片描述
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

在这里插入图片描述

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

a.假设给定无序序列结构如下
 在这里插入图片描述
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
在这里插入图片描述

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

在这里插入图片描述

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

在这里插入图片描述

b.重新调整结构,使其继续满足堆定义

在这里插入图片描述

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

在这里插入图片描述

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
在这里插入图片描述

再简单总结下堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

public static void MakeMinHeap(int a[], int n){
 for(int i=(n-1)/2 ; i>=0 ; i--){
     MinHeapFixdown(a,i,n);
 }
}
//从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2  
public static void MinHeapFixdown(int a[],int i,int n){

   int j = 2*i+1; //子节点
   int temp = 0;

   while(j<n){
       //在左右子节点中寻找最小的
       if(j+1<n && a[j+1]<a[j]){  
           j++;
       }

       if(a[i] <= a[j])
           break;

       //较大节点下移
       temp = a[i];
       a[i] = a[j];
       a[j] = temp;

       i = j;
       j = 2*i+1;
   }
}

参考https://www.cnblogs.com/rickhsg/p/3664944.html
得到建堆的复杂度为O(n)。

非比较排序

计数排序

参考
https://blog.csdn.net/csdnnews/article/details/83005778

桶排序(binsort)和基数排序(RadixSort)

基本思想:
BinSort想法非常简单,首先创建数组A[MaxValue];然后将每个数放到相应的位置上(例如17放在下标17的数组位置);最后遍历数组,即为排序后的结果。
图示:
在这里插入图片描述

BinSort
问题: 当序列中存在较大值时,BinSort 的排序方法会浪费大量的空间开销。
RadixSort
基本思想: 基数排序是在BinSort的基础上,通过基数的限制来减少空间的开销。
过程:
在这里插入图片描述
过程1
先比较个位数,存起来结果,然后比较十位数的结果,存起来,如果十位数相同,比较个位数。
在这里插入图片描述
1)首先确定基数为10,数组的长度也就是10.每个数34都会在这10个数中寻找自己的位置。
(2)不同于BinSort会直接将数34放在数组的下标34处,基数排序是将34分开为3和4,第一轮排序根据最末位放在数组的下标4处,第二轮排序根据倒数第二位放在数组的下标3处,然后遍历数组即可。

public static void RadixSort(int A[],int temp[],int n,int k,int r,int cnt[]){

   //A:原数组
   //temp:临时数组
   //n:序列的数字个数
   //k:最大的位数2
   //r:基数10
   //cnt:存储bin[i]的个数

   for(int i=0 , rtok=1; i<k ; i++ ,rtok = rtok*r){

       //初始化
       for(int j=0;j<r;j++){
           cnt[j] = 0;
       }
       //计算每个箱子的数字个数
       for(int j=0;j<n;j++){
           cnt[(A[j]/rtok)%r]++;
       }
       //cnt[j]的个数修改为前j个箱子一共有几个数字
       for(int j=1;j<r;j++){
           cnt[j] = cnt[j-1] + cnt[j];
       }
       for(int j = n-1;j>=0;j--){      //重点理解
           cnt[(A[j]/rtok)%r]--;
           temp[cnt[(A[j]/rtok)%r]] = A[j];
       }
       for(int j=0;j<n;j++){
           A[j] = temp[j];
       }
   }
}

最多需要O(n)的空间和时间复杂度。

堆排序对于topk问题是最好的。

总结

https://blog.csdn.net/qq_36770641/article/details/82669788
桶排序计数排序和基数排序都是稳定的。
下面是比较类排序的对比:
在这里插入图片描述
我们关注一下稳定性。
冒泡排序的稳定性就不需要说了。当然也是得看编程细节的交换的条件带不带等号。如果不带等号的话那么就是稳定的。
选择排序的话如果交换判断也不带等号,那么也是稳定的。
插入排序的话,也可以是稳定的。
希尔排序,
在这里插入图片描述
显然是不稳定的,如果把59换成17,那么它会被换到第一个17的上面。

堆排序
比如大顶堆的定义是父节点大于等于子节点,存在一个最大元素和尾部交换的过程,这导致了不稳定。

归并排序也是可以做到稳定的。在这里插入图片描述
只要让归并时相等的时候,左边的先放就可以保证稳定性,这个也不难做到。

快速排序也是不稳定的,因为挖坑填坑很容易打乱顺序。
在这里插入图片描述

在堆排序(小根堆)的时候,每次总是将最小的元素移除,然后将最后的元素放到堆顶,再让其自我调整。这样一来,有很多比较将是被浪费的,因为被拿到堆顶的那个元素几乎肯定是很大的,而靠近堆顶的元素又几乎肯定是很小的,最后一个元素能留在堆顶的可能性微乎其微,最后一个元素很有可能最终再被移动到底部。在堆排序里面有大量这种近乎无效的比较。随着数据规模的增长,比较的开销最差情况应该在(线性*对数)级别,如果数据量是原来的10倍,那么用于比较的时间开销可能是原来的10log10倍。
堆排序的过程中,需要有效的随机存取。比较父节点和字节点的值大小的时候,虽然计算下标会很快完成,但是在大规模的数据中对数组指针寻址也需要一定的时间。而快速排序只需要将数组指针移动到相邻的区域即可。在堆排序中,会大量的随机存取数据;而在快速排序中,只会大量的顺序存取数据。随着数据规模的扩大,这方面的差距会明显增大。在这方面的时间开销来说,快速排序只会线性增长,而堆排序增加幅度很大,会远远大于线性。

    在快速排序中,每次数据移动都意味着该数据距离它正确的位置越来越近,而在堆排序中,类似将堆尾部的数据移到堆顶这样的操作只会使相应的数据远离它正确的位置,后续必然有一些操作再将其移动,即“做了好多无用功”。

就像标准库中的sort,是通过 先快排,递归深度超过一个阀值就改成堆排,然后对最后的几个进行插入排序来实现的。

都是内部排序算法。
最后补充一点:
https://www.cnblogs.com/pxguoo/archive/2011/08/03/2126172.html

这里用信息论的角度来解释排序算法,不过有一点是错需要解释的:
然而,快速排序的第二次比较就不那么高明了:我们不妨令轴元素为pivot,第一次比较结果是a1<pivot,那么可以证明第二次比较a2也小于pivot的可能性是2/3!这容易证明:如果a2>pivot的话,那么a1,a2,pivot这三个元素之间的关系就完全确定了——a1<pivot<a2,剩下来的元素排列的可能性我们不妨记为P(不需要具体算出来)。而如果a2<pivot呢?那么a1和a2的关系就仍然是不确定的,也就是说,这个分支里面含有两种情况:a1<a2<pivot,以及a2<a1<pivot。对于其中任一种情况,剩下的元素排列的可能性都是P,于是这个分支里面剩下的排列可能性就是2P。所以当a2<pivot的时候,还剩下2/3的可能性需要排查。
我们假设a1,a2和pivot三个元素互异。
那么a1>pivot的概率是1/2没错。
三个元素共有6种升序排列,假设可能均等:
a1 a2 pivot
a1 pivot a2
a2 pivot a1
a2 a1 pivot
pivot a1 a2
pivot a2 a1
第一次比较a1和pivot,是可以排除一半的可能。
第二次a2>pivot是在第一次a1和pivot大小确定的条件下的条件概率,无论是在a1>pivot还是a1<pivot的条件下,a2和pivot关系的分支都不是均等的,一个是1/3,一个是2/3。比如假如a1>pivot,那么只有1 2 4的排列可能,在三种均等的可能中,a2>pivot的概率是2/3,也就是如果a2>pivot,那么剩下还有1和4可能,如果a1<pivot,只有2了,这种划分的不均等导致了最快的效率低下。

从信息论的角度,如果一个问题的答案可能为k个,那么将问题规模均分为k肯定是最好的。(已知区间,均匀分布的信息熵最大,获得的信息最大。)
或者说这里也似乎套用了最大熵原理,当我们对事情了解甚少时(比如排序),那么我们应该给他最大的不确定性也就是最大的信息熵,就像鸡蛋不要放在一个篮子里,这样的风险就会小。再举个例子,如果你要投资两个公司,如果事先对这两个公司都不了解,你肯定会对半投资。如果有了解,那就是有额外的信息。就像一开始排序,我们是零信息的,那么就需要最大熵才保险,这样不会偏向任何一种结果,给他大的概率。

这让我回想起二分查找的初始版本,虽然查找的结果在左右的概率都是1/2,但是比较次数却不同,如果很不幸,每次都落入了2次比较,那效率很差。
斐波那契查找通过调整左右区间的长度比来均衡比较次数,比较次数少的让它区间长点,比较次数多的区间短点,这样可以说达到了区间长度和比较次数比的一致。

基数排序的效率高是因为它每次放数直接就是确定的,没有不确定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值