数据结构和算法总结(二):排序

前言

复习各种排序算法,并记录下。

正文

1.冒泡排序

冒泡可以说是最简单的排序算法,它的排序过程就是每次遍历数组将最大的那个数往前顶,就好像气泡上浮一样。

过程可以参考如下图

参考代码

void bubbleSort(vector<int>& num)
{
    for(int i = num.size()- 1;i > 0;i--)
    {
       for(int j = 1;j <= i;j++)
       {
           if(num[j] < num[j-1])
            swap(num[j],num[j-1]);
       }
    }
}

复杂度分析

冒泡的最坏情况下的时间复杂度为:O(n2),平均复杂度:O(n2) 。

优化

我们可以稍微优化下冒泡排序,增加一个标识来确定一个数组是否有序,如果是那么可以提前终止排序,参考代码如下

//提前终止的冒泡排序
bool PreStopBubble(vector<int>& num,const int n)
{
   bool isSwapped = false;
   for(int i = 1;i < n;i++)
   {
       if(num[i - 1] > num[i])
       {
           isSwapped = true;
           cout << "Swapped!" << endl;
           swap(num[i - 1],num[i]);
       }
   }
   return isSwapped;
}

void PreStopBubbleSort(vector<int>&num)
{
  for(int i = num.size();i > 0 && PreStopBubble(num,i);i--); 
  //如果某一次冒泡过程未发生交换,那么说明数组已经有序,所以提前终止
}

这种情形下,冒泡排序的最好复杂度可以达到O(n)。

 

2.选择排序

选择排序和冒泡排序的思路类似,都是找到最大(或者最小)的数,只不过选择排序需要额外的空间来存储一次遍历过程中最大(或者最小)的数字和它的位置,然后将它与首位(或者末位)交换。

过程可以参考如下图

注:图中的选择排序每次是选择的最小值,参考代码每次是选取最大值。

参考代码

void SelectionSort(vector<int>& num)
{
    for(int i = num.size();i > 0 ;i--)
    {
        int tmp = num[0],index = 0;
        for(int j = 1;j < i;j++)
        {
           if(num[j] > tmp)
           {
               tmp = num[j];
               index = j;
           }
        }
        swap(num[index],num[i - 1]);
    }
}

复杂度分析

选择排序的最坏情况下的时间复杂度为:O(n2),平均复杂度:O(n2) 。但是往往选择排序效率优于冒泡排序(如果不是提前终止的冒泡),因为每一次遍历选择排序只需要发生一次交换,而冒泡排序可能发生了多次交换,然而,这是选择排序牺牲了额外空间来存储最值得来的。

 

3.计数排序

计数排序,也叫桶排序。这种排序方法适合已知一定范围内的数字排序,每一个数字都有一个对应的桶,一次遍历将数组的数字放入到对应的桶中进行统计,然后遍历每个桶将其输出即可。

过程可以参考如下图

参考代码

void CountSort(vector<int>& num,const int maxnum)
{
   vector<int> countBox(maxnum + 1,0);
   for(int i =0;i < num.size();i++)
   {
       countBox[num[i]]++;
   }
   int j = 0;
   for(int i = 0;i < num.size();i++)
   {
      if(countBox[j]-- > 0)
        num[i] = j;
      else
      {
         j++;i--;
      }
   }
}

复杂度分析

计数排序的平均时间复杂度为:O(n)。但是它的空间复杂度很差。这是一种牺牲空间换取时间的排序算法。

 

4.归并排序

有序数组的合并

首先,我们要知道如何将两个有序数组合并。方法很简单,从头开始比较两个数组的数字,用一个额外的数组tmp存储一次比较时的较小数字,然后较小者所在数组的索引向后+1继续和之前另一个数组的较大者比较。如果其中一个数组遍历到末尾,那么把另一个数组的剩余元素依次添加到tmp数组末尾即可,这样tmp就是一个合并后的有序数组。

分治

而对于一个无序数组,我们可以将它划分为两个无序子数组,子数组又可以不停划分,直到当一个子数组只有一个数时,这时这个子数组肯定是有序的,那么我们就可以将一个个有序子数组合并成更大的有序子数组,直到最终合并成一个有序数组,这就是归并排序的思想。

排序过程可以参考如下图

参考代码

void MergeArray(vector<int>& num,int left,int right,int mid,vector<int>& tmp)
{ //合并两个有序子数组
    int l = left,lm = mid;
    int mr = mid + 1,r = right;
    int k = 0;
    while(l <= lm && mr <= r)
    {
        if(num[l] < num[mr])
            tmp[k++] = num[l++];
        else
            tmp[k++] = num[mr++];
    }
    while(l <= lm) tmp[k++] = num[l++];
    while(mr <= r) tmp[k++] = num[mr++];
    for(int i = 0;i < k;i++)
    {
        num[left + i] = tmp[i];
    }
}

void MergeSort(vector<int>& num,int left,int right,vector<int>& tmp) 
{
   if(left >= right)
    return;
   int mid = (left + right)/2;
  //递归+分治
   MergeSort(num,left,mid,tmp);  
   MergeSort(num,mid + 1,right,tmp);
   MergeArray(num,left,right,mid,tmp);
}

void MergeSort_begin(vector<int>& num) //归并排序入口
{
    if(num.empty())
        return ;
    vector<int> tmp(num.size());
    MergeSort(num,0,num.size() - 1,tmp);
}

复杂度分析

归并排序的最坏时间复杂度为:O(nlogn),平均复杂度为:O(nlogn)。

 

5.快速排序

快速排序的思想从本质来说与归并排序类似,也是分治。只不过快速排序是在数组中选定了一个轴点,比轴点小的数字划分到左边,比轴点大的划分到右边,这样一个数组就被划分成了两部分,然后在这两部分基础上继续选择一个轴点划分,如此直到无法划分为止。

排序过程可以参考如下图

参考代码

注:这里是选取每个数组最左边的数字为轴点

int qs_partition(vector<int>& num,int left,int right)
{
  int pivot = num[left];
  int l = left,r =right;
  while(l < r)
  {
      while(l < r && num[r] >= pivot) r--;
      if(l < r)
        num[l++] = num[r];
      while(l < r && num[l] < pivot) l++;
      if(l < r)
        num[r--] = num[l];
  }
  num[l] = pivot;
  return l;
}

void quickSort(vector<int>& num,int left,int right)
{
  if(left > right)
    return;
  int k = qs_partition(num,left,right);
  quickSort(num,left,k - 1);
  quickSort(num,k + 1,right);
}

上述的为递归的快速排序,如果数据量非常大可能会导致栈内存爆掉,所以可以用一个栈来实现非递归的快速排序。

非递归的快速排序参考代码

int qs_partition(vector<int>& num,int left,int right)
{
  int pivot = num[left];
  int l = left,r =right;
  while(l < r)
  {
      while(l < r && num[r] >= pivot) r--;
      if(l < r)
        num[l++] = num[r];
      while(l < r && num[l] < pivot) l++;
      if(l < r)
        num[r--] = num[l];
  }
  num[l] = pivot;
  return l;
}

void stack_quickSort(vector<int>& num)
{
    int left = 0,right = num.size() - 1;
    if(left > right)
        return;
    stack<int> stk;
    int l,r;
    stk.push(right);
    stk.push(left);
    while(!stk.empty())
    {
        l = stk.top();stk.pop();
        r = stk.top();stk.pop();
        if(l < r)
        {
            int k = qs_partition(num,l,r);
            stk.push(k - 1);stk.push(l);
            stk.push(r);stk.push(k + 1);
        }
    }
}

复杂度分析  

快速排序的最坏时间复杂度为:O(n2),即轴点的左侧或者右侧没有数字。最好的情况是左右两侧数字大致相同,平均复杂度为:O(nlogn)。

补充

快速排序的轴点选择对于该排序算法的效率有很大的影响,最普通的选取最左或者最右的数字作为轴点的方法其实不太稳定,常用的选取轴点的方法是随机取值或者三值取中。

三值取中:顾名思义,在最左、最右、中间三个位置选取三个数字,然后在三个数字中选取值居于中间的那个数字作为轴点。

 

参考资料

《数据结构、算法与应用——C++描述》   作者:【美】 萨特吉·萨尼       机械工业出版社

  Visualgo算法可视化网站

posted @ 2018-03-18 17:01  0kk470  阅读(485)  评论(0编辑  收藏  举报