数据结构与算法之复杂度讲解和排序算法

时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度衡量的是一个算法的运行空间。在计算器前期,空间缺乏。所以对空间复杂度很在乎。但是经过计算机行业发展,由摩尔定律,硬件每十八个月就会翻一番,所以手机的内存越来越高。

(一)时间复杂度:算法的大概基本执行次数

1.大O的渐进表示法:用于描述函数渐进行为的数学符号

常数操作:int a=air[i];一条指令即可。而int b=list.get[i];得遍历整个列表,直到找到i

如果一个操作和样本的数据量没有关系,每次都是固定时间内完成的操作称为常数操作

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

1)如果最高项前面的系数存在并且不为1,就去除这个系数

O(n^2)和O(2*n*n)没区别

2)在修改后的运行次数函数中,只保留最高项

3)用常数1取代运行时间中的所有加法常数

for(k=1;k<100;k++)
{
 printf("hehe");
}//  O(n)=1

4)但是有些时间复杂度的计算会存在最好,最坏和平均的情况。

比如在数组中查找一个数,最好是一次找到,最差N次找到,平均是N/2次找到。但是一般情况下,我们关注的都是最坏情况 。

(二)排序算法具体剖析

1.冒泡排序:

最坏F(n)=1+2+3+4+...n-1=n^2 ;    最好F(n)=n;

void BubbleSort(int *a,int n)
{
  assert(a);//检查指针是否为空
  for(size_t end=n;end>0;--end)
   {
    int exchange=0;//检查是否已经排好顺序了
    for(size_t i=1;i<end;++i)
     {
      if(a[i-1]>a[i])
       {
         swap(&a[i-1],&a[i]);
         exchange=1;
       }
     }
    if(exchange==0){
     break;
    }
}        

2 选择排序

每一次的遍历过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次比较。

#include<iostream>
using namespace std;

int main()
{
  int a[10]={0};
  for(int i=0;i<10;i++)
 {
  cin>>a[i];
 }
 int min=a[0];
 for(int i=0;i<n-1;i++)
  for(int j=i+1;j<n;j++)
   {
     if(a[j]>a[i])
     swap(*a[j],*a[i])
   }
for(i=0;i<n-1;i++)
 {
  cout<<a[i]<<endl;
 }

 每次排序:第一次看数字看了N眼,比了(N-1)次,1次swap

 第二次看数字看了(N-1)眼,比了(N-2)次,1次swap

 则总共的常数操作位:a*N^2+b*N+c    时间复杂度位O(n^2)

3、插入排序

  时间复杂度不同,数据状况不同。最好O(n),最差O(n^2)

插入排序可以把数据分成已经排序的和没有排序的俩组。

找到未排序的组中的第一个元素,向已经排好序的组中插入。

倒序遍历已经排好顺序的元素,依次和待插入的元素比较,直到找到一个元素小于等于待插入的元素,那么就把这个待插入元素放到这个位置,其他的元素向后移动一位

下标:分别使得下标0~0,0~1,0~2...0~size-1所代表的值有序

void turnxu(int arr[],int size)
{
 if(arr==NULL||size<2)
    return;
 int i=0,j=0;
 for(i=1;i<size;i++)//i为无序组
  for(j=i-1;j>=0&&arr[j]>arr[j+1];j--)
   {
    swap(arr,i,j);
   }//j是有序组
}

4.归并排序

 

 

 

 

 

T(n)=2T( n/2)+O(n)=O(N*logN)//优点:没有浪费比较行为。变成了比较有序的部分去和下一个部分merge(归并)。空间复杂度O(N)

void xu(int arr[],int L,int R)
{
  if(L==R)
    return;//L是左边界,R是右边界
  int mid=L+((R-L)>>1);//二分
  xu(arr,L,mid);//对左边排序
  xu(arr,mid+1,R);//对右侧排序
  merge(arr,L,mid,R);//对整体排序 外排序
}

void merge(int arr[],int L,int M,int R)
{
  int help[]=new int[L-R+1];//开辟一个新子组进行存储
  int i=0,p1=L,p2=M+1;//p1指针指向左子组的第一个位置,p2指向右子组的第一个位置
  while(p1<=M&&p2<=R)
   help[i++]=arr[p1]<arr[p2]?arr[p1]:arr[p2];//从左右俩个数组里面找小的放进去
  while(p1<=M)
    help[i++]=arr[p1++];//p2越界而p1没有越界
  while(p2<=R)
    help[i++]=arr[p2++];
  for(i=0;i<L-R+1;i++)
    arr[L+i]=help[i];//合并到一个数组里面
 delete help[];
}
  

5,快排

 

void quicksort(int arr[],int L,int R)
{
  if(L<R)
   swap(arr,L+(int)(rand()*(R-L+1)),R);//等随机选一个数字和最右位置数字交换
   int  p[]=partition(arr,L,R);
   quicksort(arr,L,p[0]-1);//<区域 p[0]-1得到小于区域的右边界
   quicksort(arr,p[1]+1,R);//>区域 
}
//处理arr的函数,默认把arr[r]作为划分
//返回值为这个区域的左右边界,长度为2的数组 res[0],res[1]
int partition(int arr[],int L,int R)
{
  //确定分界值,并且分别定义俩个指针分别指向切分元素的最小索引处和最大索引处的下一个位置
  int key=a[0];
  int left=L;
  int right=R+1;
  
  //切分
  while(1)
  {
    //先从右边往左边扫描,移动right指针,让它找到一个比分界值小的元素停止
     while(key<a[--right]){
       if(right==L)
          break;
      }
    //再从左往右扫描,移动left指针,找到一个比分界值大的元素
     while(a[++left]<key){
        if(left==R)
           break;
      }
    if(left>=right) break;
    else swap(a,left,right);
  }
 swap(arr,L,right);
}

快排的空间复杂度是O(logN)  类似于满二叉树的展开。 最差为O(N)  

6.堆排序 

堆在逻辑上是一颗完全二叉树结构(满二叉树或者从左往右依次遍满)(数组从0开始的一段可以对应成完全二叉树)

大根堆:每某个结点为头的树的最大值是这个结点。

小根堆:每一个头结点的值是其所属这棵树的最小值。

//堆排序
void headinsert(int arr[],int index)
{
  while(arr[index]>arr[(index-1)/2])
   swap(arr,index,(index-1)/2);//跳出循环的时候是最大的确实是父节点或者是NULL
   index=(index-1)/2;
}

问题1: 返回最大值并且把最大值移除之后仍然是大根堆 

//返回最大值并且把最大值移除之后仍然是大根堆
int heapfy(int arr[],int index,int heapsize){
 //用index记录从哪里开始
 int left=index*2+1;//记录左孩子下标
  while(left<helpsize)
 {//保证下面还有数字
 //俩个孩子中谁的值大给leftlar
 int large=arr[left]>arr[left+1]?left:left+1;
 //父亲和最大儿子比较
 large=arr[index]>arr[large]?index:large;

 if(large==index)
  break;
 swap(arr,index,large);
 index=largest;
 left=index*2+1;
}
 }

7.希尔排序

插入排序的另一种,又称为”缩小增量排序“。 

 

void sort(int a[])
{
  //1.根据数组a的长度,确定增长量h和初值
  int h=1;
  while(h<sizeof(a)/sizeof(a[0])/2)
    h=2*h+1;
  //排序
  while(h>=1)
  {
    for(int i=h;i<a.size();i++)//找到待交换的元素
      for(int j=i;j<a.size();j-=h)//和前面的元素比较 将这个元素插入到前面已经排好序的数列中
      //待插入的元素是a[j],比较a[j-h]
      { if(a[j]>a[j-h])
          swap(a[j],a[j-h]);
        else  break;
      }
   h/=2;
}

排序的稳定性 

含义:数组arr中有若干元素,如果A和B的元素值相同,并且A在B的前面。如果使用某种算法之后,A依然在B的前面,则这个数列是稳定的。

意义:对多组数据排序的时候稳定性很有意义。

冒泡稳定性强,选择不强,插入稳定,希尔不稳定,归并稳定,快排不稳定

(三)递归函数的T(n)

递归:在方法内部调用方法本身。可以分解一个大的问题。每一个递归的时候都需要在栈内存开辟一块空间。如果递归的层级太多,就会造成栈溢出

master公式:T(n)=a*T(n/b)+O(N^d)

使用条件:子问题的规模等量

log以b为底a的对数<d  T(n)=O(N^d)

log以b为底a的对数>d  T(n)=O(N的log以b为底a的对数)

log以b为底a的对数=d  T(n)=O(N^d*logN)

1)T(n)代表母问题有n个子问题

2)a是每个子问题调用的次数

3)n/b代表每个子问题的规模

4)O(N^d)代表除子问题外调用的过程

1.斐波拉契数列

//递归算法复杂度=递归次数*每次递归函数的次数//O(n)
long long f(size_t N)
{
 return N<2?N:f(N-1)*N;
}
long long f(size_t N)
{
 return N<2?N:f(N-1)+f(N-2);
}//画图可得O(2^n)

改进斐波拉契数列

long l0ng*fi(int N)
{
  long long*f=malloc(sizeof(long long)*(N+1));
  f[0]=0;
  if(N==0)
     return f;
  f[1]=1;
  
  //以空间换时间
  for(int i=2;i<N;i++)
  {
     f[i]=f[i-1]+f[i-2];
  }
  return f;
    
}
long l0ng*fi(int N)
{
  long long*f=malloc(sizeof(long long)*(N+1));
  f[0]=0;
  if(N==0)
     return f;
  f[1]=1;
  
  //以空间换时间
  for(int i=2;i<N;i++)
  {
     f[i]=f[i-1]+f[i-2];
  }
  return f;
    
}

2. 例:求数组的最大值  T(n)=O(n)

int ismax(int arr[],int L,int R)//求[L,R]上数组的max
{
  if(L==R)
   return arr[L];
 int mid=L+((R-L)>>1);//求中点 直接求会导致越界,用L+(R-L)/2
 int leftmax=ismax(arr,L,mid);
 int righemax=ismax(arr,mid+1,R);
 return max(leftmax,rightmax);
}

猜你喜欢

转载自blog.csdn.net/m0_63203388/article/details/122253739