排序算法总结及C++实现

1 引入 认识时间复杂度

  常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

  时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。具体来说,在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为 f ( N ) f(N) ,那么时间复杂度为 O ( f ( N ) ) O(f(N))

  评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。

2 补充 对数器

  用来判定算法是否正确,笔试的时候不要裸奔去考,可以准备对数器,像是堆,数组,二叉树等的随机样本发生器,以快速检测出错的地方。

  对数器可以说是验证算法是否正确的一种方式。尤其是在笔试的时候,用贪心算法写出的程序,暂时无法用数学公式严格推导证明,只能通过大量的数据集验证算法的正确性。而大量的数据集当中要包括各种情况,各个方面都要考虑到,对我们自己来说,有时会考虑不周,而且又是在时间紧迫的情况下。所以对数器就派上了用场。

  1. 有一个你想测试的算法a【自己实现的排序算法】
  2. 实现一个绝对正确但复杂度高的算法b【使用#include中的sort函数】
  3. 实现一个随机样本产生器【下面实现中函数randomArrayGenerator】
  4. 实现比对算法a和b的方法,判断两个算法得出的结果是否相等【下面实现中函数isEqual】
  5. 多次(100000+)比对a和b来验证a是否正确【主函数中设置循环即可】
  6. 如果有样本出错,则打印出来分析
  7. 当对此对比测试都正确时,可以基本判断算法a正确

  其中要注意的几点:

  1. 要测试的算法a是时间复杂度比较低的算法,而算法b唯一要求就是保证正确,而不用管复杂度的高低
  2. 随机产生的样本大小要小,这里说的是样本的大小而不是样本的个数。因为出错时,小样本方便分析。
  3. 随机产生的样本个数要多,100000+ 只要大量随机产生的样本才可能覆盖所有的情况。
  4. 算法b也无法保证完全的正确,在不断出错调试的过程中,也可以不断完善b,最终达到a和b都正确的完美结果。
 /**
  随机生成随机个元素随机数字的数组
   **/
   vector<int> randomArrayGenerator(int maxSize,int maxValue)
   {
       default_random_engine e;
       uniform_real_distribution<double> u(0, 1); //随机数分布对象
       double random = u(e);
       int length = int((maxSize+1)*random*10);
       vector<int> res;
       for(int i=0;i<length;i++)
       {
           double randomval = u(e);
           res.push_back(int(randomval*(maxValue+1)));
       }
       return res;
   }
   /**
   比较两个数组是否相等
   **/
   bool isEqual(vector<int> arr1,vector<int> arr2)
   {
       if(arr1.size() != arr2.size())
           return false;
       else
       {
           for(int i=0;i<arr1.size();i++)
           {
               if(arr1[i] != arr2[i])
                   return false;
           }
       }
       return true;
   }
   /**
   复杂度很高,但是很简单的对比方法,用来对比所用方法的准确性
   可以使用#include<algorithm>中的sort函数
   **/

3 排序算法分析

关于排序的总结,这个网址的博主总结的很棒,而且设计了动画演示,对于理解有很大的帮助,以下仅作为我个人对排序算法的理解与总结,以及C++实现。

3.1 基于比较的排序

3.1.1 冒泡排序

  该算法称为冒泡排序是很形象的,质量较大的物体浮起来是需要更大的浮力的,所以每一次循环就会将最重【最大】的数沉下去,最终最轻【最小】的数浮在最上面。
  该算法在一次循环中,比较相邻的元素,如果前面的数大,就交换两个元素【最后的排序结果是从小到大】,因此每次循环只排好一个位置上的数,但是每次可以少排最后一个位置的数,因为已经排好了。
  时间复杂度 O ( N 2 ) O(N^2) ,额外空间复杂度 O ( 1 ) O(1)

/**首先实现一个交换函数**/
void Swap(int *a,int *b)
   {
       int temp = *a;
       *a = *b;
       *b = temp;
   }
 /**【实现的时候出现的问题】
  一开始传入的是数组原型,在运行时会复制一个vector,然后交换操作也在复制的vector中进行,
  传出时并没有对原数组进行修改,所以测试结果时数组并没有排序。
  要想改变原数组,需要传入数组的引用,对数组进行修改
  **/
  void bubbleSort(vector<int> &arr)
  {
      if(arr.size() < 2)
          return;
      int length = arr.size();
      for (int e = length - 1; e > 0; e--) {
		for (int i = 0; i < e; i++) {
			if (arr[i] > arr[i + 1]) {
				Swap(&arr[i],&arr[i+1]);
			}
		}
	}
 }

3.1.2 选择排序

  选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。
  【实际上,冒泡排序也是每次比较找到最大【小】的数放在最后,但是选择排序的效果应该是比冒泡排序好的,两者的差别在于每次比较数据大小之后是否进行交换,冒泡排序每次都进行交换,而选择排序则是记录最大值的索引,最后进行一次交换。】
  时间复杂度: O ( N 2 ) O(N^2) ,额外空间复杂度 O ( 1 ) O(1)

void selectionSort(vector<int> &arr)
  {
       if(arr.size() < 2)
           return;
       int length = arr.size();
       int minIndex;
       for(int i=0;i<length;i++)
       {
           minIndex = i;
           for(int j=i+1;j<length;j++)
           {
               minIndex = (arr[j]<arr[minIndex])?j:minIndex;
           }
           Swap(&arr[i],&arr[minIndex]);
       }
   }

3.1.3 插入排序

  一个新的值插入到哪个位置比较合适。构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
时间复杂度最好情况: O N O(N)
     最差情况: O N 2 O(N^2) 与数据状况有关

/**
  每一次循环和后面一位比较大小,满足要求则交换,若交换再和前面的位进行比较。
   **/
   void insertSort(vector<int> &arr)
   {
       if(arr.size() < 2)
           return;
       int length = arr.size();
       for(int i=0;i<length;i++)
       {
           for(int j=i+1;j > 0;j--)
           {
               if(arr[j-1]>arr[j])
                   Swap(&arr[j-1],&arr[j]);
           }
       }
   }

3.1.4 归并排序

  归并排序之前首先看一下递归调用,课堂上学习到的都是递归就是自己调用自己,很玄乎。实际上递归函数中,是系统在帮忙压栈,当前函数跑到了第几行,以及当前所有的变量和信息都压入系统的栈中,然后继续跑下次调用函数的过程,如果不符合递归结束条件,继续压入栈中。【栈,先进后出,所以,递归函数是倒着跑回去。】

  任何递归行为都可以改成非递归。

  分析复杂度,估计递归行为的通式:master公式,子问题的样本来量需要是一样的才可以使用该公式
T ( n ) = a T ( n b ) + O ( n d ) T(n)=a T\left(\frac{n}{b}\right)+O\left(n^{d}\right) 其中n父问题的样本量,n/b子过程的样本量,a子过程调用了多少次
公式剩余部分,除去调用子过程外的时间复杂度
log ( b , a ) &gt; d \log (b, a)&gt;d \quad \quad -\rangle 时间复杂度 O ( N l o g ( b , a ) ) O(N^{log(b,a)})
log ( b , a ) = d \log (b, a)=d\quad \quad -\rangle 时间复杂度 O ( N d l o g N ) O(N^d * logN)
log ( b , a ) &lt; d \log (b, a)&lt;d\quad \quad -\rangle 时间复杂度 O ( N d ) O(N^d)

  左边排好序,右边排好序,整体利用外排的方式排好序。整个就是一个递归过程,外排方式排好序是一个merge过程,也就是如何将左边有序序列与右边有序序列合并起来。
  外排:准备两个指针,分别指向两个数组的开头,指针指向的值较小的,指针后移【无论哪个数组就是:指针小后移】,过程中间判断什么满足条件以及要进行的操作。例如:归并排序就是指针指向的比较小的数放在准备的较大的数组空间内,将整个数组排好序。

  时间复杂度: O ( N l o g N ) O(NlogN)
  对应公式中, b = 2 , a = 2 , l o g 2 2 = 1 b=2,a=2,log_22=1 ,除去调用子过程外,merge比对较大的那个,并且返回时一个常数操作 O ( 1 ) O(1) ,所以 d = 0 d=0 ,所以对应公式归并排序的时间复杂度为此。
  额外空间复杂度: O ( N ) O(N)

 /**
   归并排序,递归排序
   左边排好序,右边排好序,整体利用外排的方式排好序。
   **/
   void mergeSort(vector<int> &arr)
   {
       if(arr.size() < 2)
           return;
       mergeSort(arr,0,arr.size()-1);
   }
   /**递归归并排序**/
   void mergeSort(vector<int> &arr,int start,int ended)
   {
       if(start == ended)
           return;
       int mid = start + (ended-start)/2;
       mergeSort(arr,start,mid);
       mergeSort(arr,mid+1,ended);
       merge(arr,start,mid,ended);
   }

   /**整合外排过程**/
   void merge(vector<int> &arr, int start, int mid, int ended)
   {
       int length = ended-start+1;
		int *help = new int[length];
		int i = 0;
		//设置两个指针,利用外排的方式整合两边的数据
		int p1 = start;
		int p2 = mid + 1;
		while (p1 <= mid && p2 <= ended) {
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
		}
		while (p1 <= mid) {
			help[i++] = arr[p1++];
		}
		while (p2 <= ended) {
			help[i++] = arr[p2++];
		}
		for (i = 0; i < length; i++) {
			arr[start + i] = help[i];
		}
}
补充题目 小和问题

在这里插入图片描述
实质在找右边有多少个数比当前的数大
整个过程既不会算少,也不会多算

/**小和问题**/
int mergeSum(vector<int> &arr)
   {
       if(arr.size() < 2)
           return 0;
       return mergeSum(arr,0,arr.size()-1);
   }

   int mergeSum(vector<int> &arr,int start,int ended)
   {
       if(start == ended)
           return 0;
       int mid = (start + ended)>>1;
       return mergeSum(arr,start,mid)+mergeSum(arr,mid+1,ended)+mergesum(arr,start,mid,ended);
   }
   int mergesum(vector<int> arr,int start,int mid,int ended)
   {
       int length = ended - start+1;
       int *temp = new int[length];
       int i = 0;
		//设置两个指针,利用外排的方式整合两边的数据
		int p1 = start;
		int p2 = mid + 1;
		int smallSum =0 ;
		while (p1 <= mid && p2 <= ended) {
			if(arr[p1] < arr[p2])
	           {
	               smallSum = smallSum + (ended-p2+1)*arr[p1];
	               cout<<smallSum<<endl;
	               temp[i++]= arr[p1++];
	           }
	           else
	           {
	               temp[i++] = arr[p2++];
	           }
		}
		while (p1 <= mid) {
			temp[i++] = arr[p1++];
		}
		while (p2 <= ended) {
			temp[i++] = arr[p2++];
		}
		for (i = 0; i < length; i++) {
			arr[start + i] = temp[i];
		}
		return smallSum;
   }
补充题目 逆序数

逆序对问题
  在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
实质: 实质右边有多少个数比当前的数小
  需要说明的是,这个问题实现在牛课上只能通过百分之50的用例,如果将count设置为全局变量更新就可以全部通过,我想不明白原因。

class Solution {
public:
    int InversePairs(vector<int> &data)
    {
        if(data.size() < 2)
            return 0;
        return mergeInverse(data,0,data.size()-1);
    }

    int mergeInverse(vector<int> &arr,int start,int end)
    {
        if(start == end)
            return 0 ;
        int mid = (end+start)>>1;
        return mergeInverse(arr,start,mid)+mergeInverse(arr,mid+1,end)+mergeInv(arr,start,mid,end);
    }

    int mergeInv(vector<int> &arr,int start,int mid,int end)
    {
        int length = end-start+1;
        int *temp = new int[length];
        int i = 0;
        int p1 = start;
        int p2 = mid+1;
        int count = 0;
        while(p1 <= mid && p2 <= end)
        {
            if(arr[p1] > arr[p2])
            {
                count = (count + (mid-p1+1))%1000000007;
                temp[i++] = arr[p2++];
            }else
            {
                temp[i++] = arr[p1++];
            }
        }
        while(p1 <= mid)
        {
            temp[i++] = arr[p1++];
        }
        while(p2 <= end)
        {
            temp[i++] = arr[p2++];
        }
        for(int j=0;j<length;j++)
        {
            arr[start+j] = temp[j];
        }
        return count;
    }
};

3.1.5 快速排序

引入
  给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
要求:额外空间复杂度O(1),时间复杂度O(N)
荷兰国旗问题
  问题升级:给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
要求:额外空间复杂度O(1),时间复杂度O(N)
Partition的实现:
  下面这段代码很关键,无论原始问题还是荷兰国旗问题,都是讲将数组分成块,设置两个指针,一个指向最左,一个最右,为smaller和bigger,left和right是分块数组的左右边界,同时left作为数组指针遍历数组。如果arr[left]< num,则Swap(&arr[++smaller],&arr[left++]); 遍历数组的left指针要加1,因为从左到右遍历,从++smaller交换过来的数字一定等于num,不需要再比较了。但是arr[left] > num,Swap(&arr[left],&arr[–bigger]);,left指针是不变的,因为从后面交换过来的值需要重新比较大小。
在这里插入图片描述

vector<int> Partition(vector<int> &arr,int left,int right)
{
    vector<int> res(2);
    if(arr.size()<= 0)
           return res;
       int smaller = left-1;
       int bigger = right;
       int num = arr[right];
       while(left < bigger)
       {
           if(arr[left]< num)
           {
               Swap(&arr[++smaller],&arr[left++]);
           }else if(arr[left] > num)
           {
               Swap(&arr[left],&arr[--bigger]);
           }else
           {
               left++;
           }
       }
       Swap(&arr[bigger],&arr[right]);
       res[0] = smaller+1;
       res[1] = bigger;
       return res;
}

快速排序 就是递归partition

经典快排:取每一次划分的最后一个数作为num, <= 放一边>的放一边,这样其实每次只搞定一个数的位置。选择的数就不参与遍历,最后再对这个数进行合理的归位,可以节省一个变量。
这样,partition返回的是一个index就好理解了,返回的是选择的那个数在排好序之后所在的位置。

改进:分三块,荷兰国旗问题,可以使原始问题更清晰 每次搞定等于num的个数的数的位置,因为可以返回等于num的中间部分的开始和结束位置,这一部分就不需要再调整了。

经典快排存在的问题:划出来的小于和等于区域不是等规模的,例如【1 2 3 4 5 6 7】,如果每次还是选择最后一个值作为划分值,那么划分的右边区域只有一个值,最终时间复杂度为 O N 2 O(N^2) , 时间复杂度与数据特点是有关系的

改进方法——>随机快排:从数组中每次随机取一个数进行划分【非常重要,三个 O N l o g N O(Nlog N) 中是效果最好的】。将这个随机选择的数和最后一个位置上的数交换,然后进行经典快排的算法。
长期期望复杂度 O N l o g N O(Nlog N)
额外空间复杂度 O l o g N O(log N) ,也是长期期望复杂度【需要记下来等于区域开始和结束位置,也就是划分点,才能继续去搞子过程,为什么是 l o g N logN ,相当于儿二叉树划分,最多划分的次数,最差需要划分 l o g N logN 次,所以最多需要这么多空间记录】
快速排序与数据状况高度相关,如何绕开数据状况:1.随机打乱数据状况 2.哈希技巧来规避原来数据的特点
工程上,快排不是递归的,工程上不会使用递归,递归函数代价比较高。库函数里一般都是高度优化过的非递归版本。

/**快速排序**/
void quickSort(vector<int> &arr)
{
    if(arr.size() < 2) return;
    quickSort(arr,0,arr.size()-1);
}

void quickSort(vector<int> &arr,int left,int right)
{
    if(left<right)
       {
           vector<int> tmp = Partition(arr,left,right);
           quickSort(arr,left,tmp[0]-1);
           quickSort(arr,tmp[1]+1,right);
       }
}

3.1.6 堆排序

:一颗完全二叉树,满二叉树或者从左往右是依次补齐的二叉树都是堆。
  堆可以用数组来实现,从而产生逻辑结构上的树。
  根据坐标来找到父节点子节点的关系,子节点:2i+1 2i+2 父节点:(i-1)/2
大根堆:任何一颗子树的最大值都是这个子树的头部【小根堆:任何一颗子树的最小值都是这个子树的头部】
建立大根堆:【从下向上调整】
  每进来一个新的数字,根据坐标变换,找到自己的父节点,如果新进来的节点比父节点的大,进行数据交换,然后继续向上与父节点比较,直到这条路径到达根节点。【所有的交换都在数组中】

/**构造堆**/
void heapInsert(vector<int> &arr,int index)
{
    while(arr[index]>arr[(index-1)/2])
       {
           Swap(&arr[index],&arr[(index-1)/2]);
           index = (index-1)/2;
       }
}

堆化:Heapify【从上向下调整】
  堆大小为heapisize,数组的下标从0—heapisize-1,如果其中某个index对应的值发生了变化,应该如何调整堆。【一开始不太明白为什么会发生变化,其实就是再堆排序的过程中会发生Swap交换操作,这样就会将内部数据就会有变化,需要重新调整堆】

调整过程为:首先要直到index之前的肯定是顺序的,所以向下查找,找到index节点的左孩子,如果左节点在数组范围内,就看右孩子是否存在,如果存在就比较index,左孩子,及右孩子的大小找到最大的,如果最大的就是index,直接退出,这就是一个大顶堆。如果不是index,就找到最大的进行交换,然后,index= largest,left = 2*index+1继续交换。

/**堆化**/
void heapify(vector<int> &arr,int index,int length)
{
       int left = 2*index + 1;
       while(left < length)
       {
           int largest = (left+1)<length && arr[left+1]>arr[left]?(left+1):left;
           largest = arr[index]>arr[largest]?index:largest;
           if(largest == index)
               break;
           Swap(&arr[index],&arr[largest]);
           index = largest;
           left = 2*index+1;
       }
}

应用
  泡泡产生器,一直生成很多数,如何迅速找到这些数的中位数。

  准备两个数组,一个大根堆,一个小根堆,期望最好大根堆有最小的n/2个数,小根堆有最大的n/2个数,这样可以随时找到中位数。如果一个堆的数多了,进行减堆操作,将最后一个值【需要使堆顶的那个数无效】和堆顶交换,然后堆的大小减1,然后重新调整现在的堆【用到堆化函数】。【减堆操作是拿堆顶的元素,因为下面的值并不严格顺序的,所以大顶堆存最小的n/2,小顶堆存最大的n/2】。
堆,堆调整的代价只与层数有关,也就是 l o g N logN ,所以堆的应用很广泛。

堆排序:利用堆结构进行排序
  首先将一个数组变成堆结构,但是这时不一定是有序的。然后,将顶点与最后一个值进行交换,堆大小减1,然后重新Heapify新堆,重复上述过程。

  然后就从小到大有序了。时间复杂度 O ( N l o g N ) O(NlogN)

void heapSort(vector<int> &arr)
{
    if(arr.size() < 2) return;
    //首先建立一个大顶堆
    for (int i=0;i<arr.size();i++)
       {
           heapInsert(arr,i);
       }
       //然后每次取最大的数,再调整堆
       int lenght = arr.size();
       while(lenght > 0)
       {
           Swap(&arr[0],&arr[--lenght]);
           heapify(arr,0,lenght-1);
       }
}

堆总结

  1. 两种调整方式,堆结构的heapInsert和heapify
  2. 堆结构的增大和缩小
  3. 如果只是建立堆的过程,时间复杂度位O(N)
  4. 优先级队列结构

注:
优先级队列:在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue) 。
也就是堆排序中,返回根节点,以及插入对象重新堆化。所以优先级队列结构就是堆排序。

3.2 非基于比较的排序

  1. 非基于比较的排序,与被排序的样本的实际数据状况很有关系,所 以实际中并不经常使用
  2. 时间复杂度O(N),额外空间复杂度O(N)
  3. 稳定的排序

3.2.1 计数排序

  例子:一个数组里面都是1-60的,那么生成一个长度为60的数组,遍历数组,在固定位置上遇到相应的数字++,之前看到过一个对一个公司员工年龄进行排序的题目,与这个问题类似。【为什么快,因为可以建立一个很小的数组,占用很小的额外空间,换取时间复杂度,如果公司人员很多会很简洁。】《剑指offer》P81
  技术排序核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

  • 找出待排序的数组中最大和最小的元素,根据其长度生成数组C;
  • 统计数组中每个值为 i i 的元素出现的次数,存入数组C的第 i i 项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加),根据其大小,生成排序的目标数组;
  • 填充目标数组:将C中元素按顺序放到目标数组中,每放一个就将 C ( i ) C(i) 减去1,每个 C ( i ) C(i) 减到0继续放下一个数。
/**计数排序**/
vector<int> countingSort(vector<int> arr,int maxValue)
{
	//maxValue 数组中的最大值
    vector<int> bucket(maxValue+1);
    int sortedIndex = 0;
    int length = arr.size();
    int bucketLength = maxValue+1;

    for (int i = 0; i < length; i++)
    {
        if (!bucket[arr[i]])
        {
            bucket[arr[i]] = 0;
        }
        bucket[arr[i]]++;
    }

    for (int j = 0; j < bucketLength; j++)
    {
        while(bucket[j] > 0)
        {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
    return arr;
}

  计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

3.2.2 桶排序

  桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

  桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

题目:给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用非基于比较的排序。
借用桶排序思想,但是没有没有进行桶排序

  • 准备N+1个桶【N为数组大小】
  • 先遍历整个数组找到最小值和最大值,如果最小值和最大值相等,返回0;如果不等,最小值放在0号桶,最大值放在第N个桶,
  • 其他的数按照(MAX-MIN)/(N-1)进行分配,按照区间放在相应的桶里,中间必然存在一个空桶,左右都会找到一个离它最近的非空桶,这种设计保证差值最大一定来自不同的桶,但不是一定为空桶的左右为最大差值。
    【为什么可以可以保证相邻两数差值最大一定来自不同的桶?】
    如下图所示,相邻两数可能来自同一个桶,如11和15,也可能来自不同的桶,如15和21.设置一个空桶之后,左右一定存在一个离他最近的非空桶,如图的第二行,左边非空桶的最大值与右边非空桶的最小值一定是相邻的元素,它们的差值min-max一定大于单个桶所表示的范围,而桶内部元素差值一定小于每个桶所表示的范围,所以可以保证相邻元素的最大差值一定来自不同的桶,方便后面遍历查找最大差值。
    在这里插入图片描述
    其中:每个桶只更新最小值和最大值数组,以及一个记录是否进来过数的bool值数组,每一个桶找前一个非空桶的最大值与自己桶的最小值的差,用一个全局变量max记录这个值,遍历完就是结果。

  从头到尾实现该题目,同时利用对数器进行测试。

/*
给定一个数组,求如果排序之后,相邻两数的最大差值,
要求时 间复杂度O(N),且要求不能用非基于比较的排序
*/
#include <limits.h>
#include<iostream>
#include<math.h>
#include<algorithm>
#include<vector>
#include <random>

using namespace std;

//返回当前数所处的组数
int bucket(long num, long len, long min, long max){
    return (int)((num - min) * len / (max - min));
}

//要求时间复杂度O(N),且非基于比较的排序。
int maxGap(vector<int> nums)
{
    if (nums.size() < 2) {//如果数组为空或长度小于2,则返回0
        return 0;
    }
    int length = nums.size();
    int Min = INT_MAX;
    int Max = INT_MIN;
    for (int i = 0; i < length; i++) {
        Min = min(Min, nums[i]);
        Max = max(Max, nums[i]);
    }
    if (Min == Max) {
        return 0;
    }
    int *hasNum = new int[length + 1];
    int *maxs = new int[length + 1];
    int *mins = new int[length + 1];
    for (int i = 0; i < length + 1; i++){
        hasNum[i] = 0;//0表示该位置没有元素,即没有对应的max和min值
    }

    int bid = 0;
    for (int i = 0; i < length; i++) {
        bid = bucket(nums[i], length, Min, Max);//当前数处在哪个桶
        mins[bid] = hasNum[bid] ? min(mins[bid], nums[i]) : nums[i];
        maxs[bid] = hasNum[bid] ? max(maxs[bid], nums[i]) : nums[i];
        hasNum[bid] = 1;//1表示该位置有对应的max和min值
    }

    int res = 0;
    int lastMax = maxs[0];
    int i = 1;
    for (; i <= length; i++) {
        if (hasNum[i]) {
            res = max(res, mins[i] - lastMax);
            lastMax = maxs[i];
        }
    }
    return res;
}


//该方法为标准方法,复杂但一定正确的方法
int comparator(vector<int> arr)
{
    if(arr.size() < 2)//如果数组为空或长度小于2,则返回0
        return 0;
    vector<int> temp;
    for(int i = 0; i < arr.size(); i ++)
        temp.push_back(arr[i]);
    sort(temp.begin(),temp.end());
    for(int i = 0; i < temp.size(); i ++)
        arr[i] = temp[i];
    int gap = INT_MIN;
    for(int i = 1; i < temp.size(); i ++)
        gap = max(gap, arr[i] - arr[i - 1]);
    return gap;
}

/**
随机生成随机个元素随机数字的数组
**/
vector<int> randomArrayGenerator(int maxSize,int maxValue)
{
    default_random_engine e;
    uniform_real_distribution<double> u(0, 1); //随机数分布对象
    double random = u(e);
    int length = int((maxSize+1)*random*10);
    vector<int> res;
    for(int i=0;i<length;i++)
    {
        double randomval = u(e);
        res.push_back(int(randomval*(maxValue+1)));
    }
    return res;
}

/**
比较两个数组是否相等
**/
bool isEqual(vector<int> arr1,vector<int> arr2)
{
    if(arr1.size() != arr2.size())
        return false;
    else
    {
        for(int i=0;i<arr1.size();i++)
        {
            if(arr1[i] != arr2[i])
                return false;
        }
    }
    return true;
}

void printArray(vector<int> arr){
    if(arr.empty())
        return;
    for(int i = 0; i < arr.size(); i ++)
        cout<<arr[i]<< " ";
    cout<<endl;
}

int main()
{
    int testTime = 50;
    int maxSize = 100;
    int maxValue = 100;
    bool succeed = true;
    for(int i = 0; i < testTime; i ++){
        vector<int> arr1 = randomArrayGenerator(maxSize, maxValue);
        vector<int>  arr2 = arr1;
        if(maxGap(arr1) != comparator(arr2)){
            succeed = false;
            break;
        }
    }
    cout<<(succeed ? "Nice!" : "Fucking Fucked!")<<endl;
    vector<int> arr = randomArrayGenerator(maxSize, maxValue);
    cout<<"随机生成的数组:"<<endl;
    printArray(arr);
    cout<<"排序之后相邻元素最大gap为:"<<maxGap(arr)<<endl;
    return 0;
}

3.2.3 基数排序

  基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

  基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

  基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

4 排序总结

4.1 稳定性

稳定性,简单来说,数据中相同的值,在排序后相对顺序是否会被打乱,如2 2 2 ,重新排序后第一个位置的2会不会变到第二个位置。
【现实世界的业务往往需要稳定性, 如自己设置的学生名词排序等等】
-------------------------------------------------------------------O(N)算法
冒泡:如果碰到相同的值不交换,就可以保证稳定性
选择排序:不能做到稳定排序,因为交换的时候可能会把相等数据交换到后面
插入排序:可以实现成稳定的,碰到相等的值不交换
-------------------------------------------------------------------O(NlogN)
归并排序:可以,merge的时候,左边和右边相等,先拷贝左边的
快速排序:不可以,partition做不到稳定,因为index数据是随机选择的【论文级别的可以实现稳定性】
堆排序:不可以,完全二叉树,交换的过程中,不care相等值

4.2 工程中的综合排序算法

工程中的综合排序算法:【综合排序特别综合】
综合排序特别综合,会整合所有算法的优点:
例如:
如果数组长度很短,会选择插入排序,常数项很小
归并和插排合并使用,当归并部分二分数据集小于一定数量的时候使用插入排序
如果数据类型是基础类型,使用快排,相同的数字没有先后顺序,没有差别
如果是自己定义的类型,如果学生,员工等类结构数据,使用归并排序,因为要保留原始数据的顺序【从稳定性出发】

4.3 有关排序问题的补充:

  1. 归并排序的额外空间复杂度可以变成O(1),但是非常难,【可以搜索归并排序,内部缓存法】
  2. 快速排序可以做到稳定性,但是非常难【搜索 01 stable sort】
  3. 一个面试题:奇数放左边,偶数放右边,保证奇数之间,偶数之间维持原来的顺序,要求:额外空间复杂度O(1) 时间复杂度O(N)。【如果可以实现这个问题,保证复杂度要求,这就是一个快速排序问题,但是日常快速排序做不到稳定性,要实现就是一个01问题,是非常复杂的】

4.4 认识比较器的使用:

C++中重载运算符就是比较器的实现
如果题目中,排序之后的解决方法才是重点,那么可以用系统方法来实现,例如#include中的sort函数
但是自己数据结构可能是类什么的,这时只需要实现自己的比较器,如下实现student类根据id和age分类的比较器。

public static class IdAscendingComparator implements Comparator<Student> {

@Override
	public int compare(Student o1, Student o2) {
		return o1.id - o2.id;
	}

}

public static class AgeAscendingComparator implements Comparator<Student> {

	@Override
	public int compare(Student o1, Student o2) {
		return o1.age - o2.age;
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_35479108/article/details/89452505
今日推荐