菜鸟算法入门

算法学习(二)常用数据结构和算法

2019-08-25  本文已影响0人  孔雨露

1. 常用数据结构

1. 1 数组

int[] data = new int[100];data[0]  = 1;

1、按照索引查询元素速度快
2、按照索引遍历数组方便

1、数组的大小固定后就无法扩容了
2、数组只能存储一种类型的数据
3、添加,删除的操作慢,因为要移动其他的元素。

频繁查询,对存储空间要求不大,很少增加和删除的情况。

1. 2 字典


1. 3 链表


链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;

因为含有大量的指针域,占用空间较大;
查找元素需要遍历链表来查找,非常耗时。

数据量较小,需要频繁增加,删除操作的场景

1. 4 堆栈

1.4.1 堆

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树。

1.4.2 栈


1.4.2.1 栈的定义和基本运算

栈是只能通过访问它的一端来实现数据存储和检索的一种线性数据结构。换句话说,栈的修改是按先进后出的原则进行的。因此,栈又称为先进后出(FILO)的线性表。在栈中进行插入和删除操作的一端称为栈顶(top),相应地,另一端称为栈底(bottom)。不含数据元素的栈称为空栈。

  1. 初始化栈InitStack(S):创建一个空栈S。
  2. 判断空栈StackEmpty(S):当栈为空栈时返回“真”,否则返回“假”。
  3. 入栈Push(S,x):将元素x加入栈顶,并更新栈顶指针。
  4. 出栈Pop(S):将栈顶的元素从栈中删除,并且更新栈顶指针。
  5. 读栈顶元素Top(S):返回栈顶元素的值,但不修改栈顶的指针。

1.4.2.2 栈的存储结构

栈的顺序存储是指用一组地址连续的存储单元依次存储自栈顶到栈底的数据元素,同时附设指针top指示栈顶元素的位置。采用顺序存储结构的栈也称为顺序栈。在顺序存储方式下,需要预先定义或申请栈的存储空间,也就是说栈空间的容量是有限的。因此在顺序栈中,当一个元素入栈时,需要判断是否栈满(即栈空间中没有空闲单元),若栈满,则元素入栈会发生上溢现象。

栈的链式存储是为了克服顺序存储的栈可以存在上溢的不足,可以用链表存储栈中的元素。用链表作为存储结构的栈也称为链栈。由于栈中元素的插入和删除仅在栈顶一端进行,因此不必另外设置头指针,链表的头指针就是栈顶指针。


栈的链式存储

1.4.2.3 栈的应用

  1. 表达式求值

计算机在处理算术表达式时,可将表达式先转换为后缀形式,然后利用栈进行计算。例如,表达式“46+5 (120-37)”的后缀表达式为“46 5 120 37 - * +”。计算后缀表达式时,从左至右扫描表达式:若遇到运算对象,则压入栈中;遇到运算符,则从栈顶弹出运算对象进行计算,并将运算结果压入栈中。重复以上过程,直到后缀表达式扫描结束。例如“46 5 120 37 - * +”的计算过程为:
1、依次将46,5,120,37压入栈中;
2、遇到“-”取37,120,计算120-37,得83,将其压入栈中;
3、遇到“
”取出83,5,计算83*5,得415,将其压入栈中;
4、遇到“+”,取出415,46,计算46+415,得461,将其压入栈中;
5、表达式结束,计算过程完成。
下面函数computing(char expr[],int result)的功能是基于栈计算后缀形式的表达式(以串形式存入字符数组expr)的值,并通过参数result带回该值。函数的返回值为-1/0,分别表示表达时有/无错误。假设表达式中仅包含数字,空格和算术运算符号,其中所有项均以空格分隔,且运算符仅包含(“+”),(“-”),(“”),(“\”)。
void InitStack(STACK *s):初始化栈。
void Push(STACK *s, int e):将一个整数压入栈,栈中元素数目增加1。
void Pop(STACK *s);栈顶元素出栈,栈中元素数目减1。
int Top(STACK s):返回非空栈的栈顶元素值,栈中元素不变。
int IsEmpty(STACK  s);若s是空 栈,则返回1;否则返回0。

实例1423实现代码:

int computing (char expr[], int * result)
{
       STACK s;  int  tnum,a,b;     char *ptr;
       InitStack(&s);
       ptr = expr;            /*字符指针指向后缀表达式串的第一个字符*/
       while (*ptr != '\0')
        { 
             if ( *ptr == ' ') /*当前字符是空格,则取下一个字符*/
              {
                     ptr++;
                     continue;
              }
             else  if ( isdigit(*ptr)) /*当前字符是数字,则将数字串转化为数值*/
                     {
                          tmun = 0;
                          while(*ptr >= '0'  &&  *ptr <= '9')
                          {
                                 tnum = tnum *10 + *ptr - '0';
                                 ptr++;
                          }
                            Push(&s,tnum);
                     }
             
            else  if ( *ptr == '+' || *ptr == '-'  ||  *ptr == '*'  || *ptr == '/') /*当前是运算符*/
            {
                   if ( !IsEmpty(s)){
                           a = Top(s);Pop(&s);                /*取运算符的第二个运算符,存入a*/
                           if ( IsEmpty(s)) {
                                 b = Top(s); Pop(&s);        /*取运算符的第一个运算符,存入b*/
                           }
                           else
                           {
                                 return -1;
                           }
                    }
                    else
                    {
                          retun -1;          /*栈空*/
                    }
                    switch ( *ptr) {
                         case '+' : Push(&s,b+a);break;
                         case '-':  Push(&s,b-a);break;
                         case '*':  Push(&s,b*a);break;
                         case '/': Push(&s,b/a);break;
                      }
            }
           else   /*非法字符*/
            {
                   return -1;
            }
             
             ptr++;
  }/*while*/
 
   if (IsEmpty(s))
      return -1;
  else
     {
         *result = Top(s);Pop(&s); /*得到运算结果*/
         if (!IsEmpty(s) 
           return -1;
       retrun 0;
     }
}

1.5 队列

顺序队列

1.5.1 优先队列


1.5.2 循环队列


1.6 树

1.6.1 二叉树

1、每个结点最多有两颗子树,结点的度最大为2。
2、左子树和右子树是有顺序的,次序不能颠倒。
3、即使某结点只有一个子树,也要区分左右子树。


二叉树既有链表的好处,也有数组的好处,是两者的优化方案

在处理大批量的动态数据方面非常有用。

更多二叉树相关知识请参考这篇博客:二叉树总结创建,遍历

1.6.2 二叉搜索树


1.6.3 平衡二叉树


1.7 图

无向图 有向图

更多图相关知识可以参考这篇文章:数据结构—图

1.8 散列表


更多散列表相关知识参考这篇博客:数据结构—散列表

2. 常用算法

2.1 查找算法

2.1.1 二分查找

2.1.2 广度优先搜索算法

2.1.3 深度优先搜索算法

2.2 排序算法

术语 含义
稳定 如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定 如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
内排序 所有排序操作都在内存中完成;
外排序 由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
时间复杂度 一个算法执行所耗费的时间。
空间复杂度 运行完一个程序所需内存的大小。

2.2.1 排序算法简介

排序算法分类

2.2.2 排序算法比较

2.2.2.1 稳定性比较

2.2.2.2 时间复杂度比较

对比项 平均情况 最好情况 最坏情况 是否稳定 空间
基数排序 O(n) O(n) O(n) 不稳定 需要 O(n) 额外存储空间
快速排序 O(nlogn) O(nlogn) O(n2) 不稳定
希尔排序 O(n1.5) O(n) O(n1.5) 不稳定
选择排序 O(n2) O(n2) O(n2) 不稳定
堆排序 O(nlogn) O(nlogn)) O(nlogn) 不稳定
插入排序 O(n2) O(n) O(n2) 稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定 需要 O(n) 额外存储空间
冒泡排序 O(n2) O(n2) O(n2) 稳定
二叉树排序 O(nlogn) O(nlogn) O(nlogn) 稳定
计数排序 O(n+k) O(n+k) O(n+k) 稳定 需要 O(n+k) 额外存储空间,k为序列中Max-Min+1
桶排序 O(n) O(n) O(n) 需要 O(k) 额外存储空间

2.2.2.3 辅助空间比较

2.2.2.4 其他比较

2.2.3 排序算法实现

2.2.3.1 插入排序

算法简介

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  4. 将新元素插入到该位置后;
  5. 重复步骤2~5。
插入排序
算法实现
void insertion_sort (int a[], int n) {
    int i,j,v;
    for (i=1; i<n; i++) {      //如果第i个元素小于第j个,则第j个向后移动
        for (v=a[i], j=i-1; j>=0&&v<a[j]; j--)
            a[j+1]=a[j];
        a[j+1]=v;
    }
}
/*插入排序*/
void insertSort(vector<int> &arr, int bgn, int end)
{
    for (int i = bgn + 1; i < end; ++i)
    {
        /*
        * 分为1,2两部分处理,可以囊括j = beg - 1时的情况
        * 即需要将arr[i]插入到首元素前的位置,若使用一个for
        * 包括这两部分,则会在发生这种情况时退出
        */
        /*1*/
        int j = i - 1;
        for ( ; j >= bgn; --j)
            if (arr[j] <= arr[I])
                break;
        /*2*/
        if (j != i - 1)
        {
            int temp = arr[i];
            for (int k = i; k > j + 1; --k)
            {
                arr[k] = arr[k - 1];
            }
            arr[j + 1] = temp;
        }
    }
}

2.2.3.2 选择排序

算法简介

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  1. 初始状态:无序区为R[1..n],有序区为空;
  2. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  3. n-1趟结束,数组有序化了。
算法实现
void selection_sort (int a[], int n) {
    int i,j,pos,tmp;
    for (i=0; i<n-1; i++) {      //寻找最小值的下标
        for (pos=i, j=i+1; j<n; j++)
            if (a[pos]>a[j])
                pos=j;
        if (pos != i) {
            tmp=a[I];
            a[i]=a[pos];
            a[pos]=tmp;
        }
    }
}

2.2.3.3 冒泡排序

算法简介

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。
算法实现
void bubble_sort (int a[], int n) {
    int i, j, lastSwap, tmp;
    for (j=n-1; j>0; j=lastSwap) {        lastSwap=0;     //每一轮要初始化为0,防止某一轮未发生交换,lastSwap保留上一轮的值进入死循环
        for (i=0; i<j; i++) {
            if (a[i] > a[i+1]) {
                tmp=a[I];
                a[i]=a[i+1];
                a[i+1]=tmp;
           //最后一次交换位置的坐标
                lastSwap = I;
            }
        }
    }
}

2.2.3.4 快速排序

算法简介

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。
具体算法描述如下:

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
快速排序
算法实现
int mpartition(int a[], int l, int r) {
    int pivot = a[l];
 
    while (l<r) {
        while (l<r && pivot<=a[r]) r--;
        if (l<r) a[l++]=a[r];
        while (l<r && pivot>a[l]) l++;
        if (l<r) a[r--]=a[l];
    }
    a[l]=pivot;
    return l;
}
 
void quick_sort (int a[], int l, int r) {
 
    if (l < r) {
        int q = mpartition(a, l, r);
        msort(a, l, q-1);
        msort(a, q+1, r);
    }
}

C++ 实现代码:

/*快排*/
void quickSort(vector<int> &arr, int bgn, int end)  //arr must be the reference of real param
{
    //数组arr空or仅有一个元素则退出
    if (bgn >= end - 1)
        return;

    int lindex = bgn;
    int rindex = end - 1;
    int std = arr[lindex];
    while (lindex < rindex)
    {
        while (lindex < rindex)
        {
            if (arr[rindex] < std)
            {
                arr[lindex++] = arr[rindex];
                break;
            }
            --rindex;
        }

        while (lindex < rindex)
        {
            if (arr[lindex] >= std)
            {
                arr[rindex--] = arr[lindex];
                break;
            }
            ++lindex;
        } 
    }

    arr[lindex] = std;
    quickSort(arr, bgn, lindex);
    quickSort(arr, rindex + 1, end);
}

2.2.3.5 堆排序

算法简介

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

  1. 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  3. 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,
  4. 然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。
  5. 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
堆排序-过程1 堆排序-过程2
算法实现
void heapAdjust(int a[], int i, int nLength)
{
    int nChild;
    int nTemp;
    for (nTemp = a[i]; 2 * i + 1 < nLength; i = nChild)
    {
        // 子结点的位置=2*(父结点位置)+ 1
        nChild = 2 * i + 1;
        // 得到子结点中较大的结点
        if ( nChild < nLength-1 && a[nChild + 1] > a[nChild])
            ++nChild;
        // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点
        if (nTemp < a[nChild])
        {
            a[i] = a[nChild];
            a[nChild]= nTemp;
        }
        else
        // 否则退出循环
            break;
    }
}
 
// 堆排序算法
void heap_sort(int a[],int length)
{
    int tmp;
    // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素
    //length/2-1是第一个非叶节点,此处"/"为整除
    for (int i = length / 2 - 1; i >= 0; --i)
        heapAdjust(a, i, length);
    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (int i = length - 1; i > 0; --i)
    {
        // 把第一个元素和当前的最后一个元素交换,
        // 保证当前的最后一个位置的元素都是在现在的这个序列之中最大的
      ///  Swap(&a[0], &a[I]);
          tmp = a[I];
          a[i] = a[0];
          a[0] = tmp;
        // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
        heapAdjust(a, 0, i);
    }
}

2.2.3.6 归并排序

算法简介

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。
算法实现
void mergearray(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid + 1;
    int m = mid,   n = last;
    int k = 0;
 
    while (i <= m && j <= n)
    {
        if (a[i] <= a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }
 
    while (i <= m)
        temp[k++] = a[i++];
 
    while (j <= n)
        temp[k++] = a[j++];
 
    for (i = 0; i < k; I++)
        a[first + i] = temp[I];
}
void merge_sort(int a[], int first, int last, int temp[])
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        merge_sort(a, first, mid, temp);    //左边有序
        merge_sort(a, mid + 1, last, temp); //右边有序
        mergearray(a, first, mid, last, temp); //再将二个有序数列合并
    }
}

2.2.3.7 希尔排序

算法简介

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

  1. 我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
  2. 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,
    具体算法描述:
  3. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  4. 按增量序列个数k,对序列进行k 趟排序;
  5. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
算法实现
希尔排序
void shell_sort(int a[], int n)
{
    int d, i, j, temp; //d为增量
    for(d = n/2;d >= 1;d = d/2) //增量递减到1使完成排序
    {
        for(i = d; i < n;i++)   //插入排序的一轮
        {
            temp = a[I];
            for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d)
            {
                a[j + d] = a[j];
            }
        a[j + d] = temp;
        }
    }
}

2.2.3.8 二叉树排序

算法简介
算法实现
int arr[] = {7, 8, 8, 9, 5, 16, 5, 3,56,21,34,15,42};
 
struct BST{
    int number; //保存数组元素的值
    struct BST* left;
    struct BST* right;
};
 
void insertBST(BST** tree, int v) {
    if (*tree == NULL) {
        *tree = new BST;
        (*tree)->left=(*tree)->right=NULL;
        (*tree)->number=v;
        return;
    }
    if (v < (*tree)->number)
        insertBST(&((*tree)->left), v);
    else
        insertBST(&((*tree)->right), v);
}
 
void printResult(BST* tree) {
    if (tree == NULL)
        return;
    if (tree->left != NULL)
        printResult(tree->left);
    cout << tree->number << "  ";
    if (tree->right != NULL)
        printResult(tree->right);
}
 
void createBST(BST** tree, int a[], int n) {
    *tree = NULL;
    for (int i=0; i<n; I++)
        insertBST(tree, a[I]);
}
 
int main()
{
    int n = sizeof(arr)/sizeof(int);
 
    BST* root;
    createBST(&root, arr, n);
    printResult(root);
 
}

2.2.3.9 计数排序

算法简介

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
计数排序-过程

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

算法实现

计数排序的步骤:

  1. 找出待排序的数组中最大和最小的元素(计数数组C的长度为max-min+1,其中位置0存放min,依次填充到最后一个位置存放max)
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1(反向填充是为了保证稳定性)

以下代码中寻找最大和最小元素参考编程之美,比较次数为1.5n次。
计数排序适合数据分布集中的排序,如果数据太分散,会造成空间的大量浪费,假设数据为(1,2,3,1000000),这就需要1000000的额外空间,并且有大量的空间浪费和时间浪费。

void findArrMaxMin(int a[], int size, int *min, int *max)
{
    if(size == 0) {
        return;
    }
    if(size == 1) {
        *min = *max = a[0];
        return;
    }
 
    *min = a[0] > a[1] ? a[1] : a[0];
    *max = a[0] <= a[1] ? a[1] : a[0];
 
 
    int i, j;
    for(i = 2, j = 3; i < size, j < size; i += 2, j += 2) {
        int tempmax = a[i] >= a[j] ? a[i] : a[j];
        int tempmin = a[i] < a[j] ? a[i] : a[j];
 
        if(tempmax > *max)
            *max = tempmax;
        if(tempmin < *min)
            *min = tempmin;
    }
 
    //如果数组元素是奇数个,那么最后一个元素在分组的过程中没有包含其中,
    //这里单独比较
    if(size % 2 != 0) {
        if(a[size -1] > *max)
            *max = a[size - 1];
        else if(a[size -1] < *min)
            *min = a[size -1];
    }
}
 
void count_sort(int a[], int b[], int n) {
    int max, min;
    findArrMaxMin(a, n, &min, &max);
    int numRange = max-min+1;
    int* counter = new int[numRange];
 
    int i, j, k;
    for (k=0; k<numRange; k++)
        counter[k]=0;
 
    for (i=0; i<n; I++)
        counter[a[i]-min]++;
 
    for (k=1; k<numRange; k++)
        counter[k] += counter[k-1];
 
    for (j=n-1; j>=0; j--) {
        int v = a[j];
        int index = counter[v-min]-1;
        b[index]=v;
        counter[v-min]--;
    }
}

2.2.3.10 桶排序

算法简介

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

  1. 人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
  4. 从不是空的桶里把排好序的数据拼接起来。
  5. 注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。

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

算法实现
/*桶排序*/
void bucketSort(vector<int> &arr)
{
    int max = getMaxValue(arr);
    int *pBuf = new int[max + 1];

    memset(pBuf, 0, (max + 1)*sizeof(int));
    for (auto const i : arr)
        ++pBuf[I];

    for (int i = 0, j = 0; i <= max; ++i)
    {
        while (pBuf[i]--)
            arr[j++] = i;
    }
    delete []pBuf;
}

2.2.3.11 基数排序

算法简介

基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

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

基数排序有两种方法:
MSD 从高位开始进行排序 LSD 从低位开始进行排序
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶
计数排序:每个桶只存储单一键值
桶排序:每个桶存储一定范围的数值

算法实现
  1. 从个位开始,根据0~9的值将数据分到10个桶桶,例如12会划分到2号桶中。
  2. 将0~9的10个桶中的数据顺序放回到数组中。
  3. 重复上述过程,一直到最高位。
int getNumInPos(int num,int pos) //获得某个数字的第pos位的值
{
    int temp = 1;
    for (int i = 0; i < pos - 1; I++)
        temp *= 10;
 
    return (num / temp) % 10;
}
 
#define RADIX_10 10    //十个桶,表示每一位的十个数字
#define KEYNUM 5     //整数位数
void radix_sort(int* pDataArray, int iDataNum)
{
    int *radixArrays[RADIX_10];    //分别为0~9的序列空间
    for (int i = 0; i < RADIX_10; I++)
    {
        radixArrays[i] = new int[iDataNum];
        radixArrays[i][0] = 0;    //index为0处记录这组数据的个数
    }
 
    for (int pos = 1; pos <= KEYNUM; pos++)    //从个位开始到31位
    {
        for (int i = 0; i < iDataNum; i++)    //分配过程
        {
            int num = getNumInPos(pDataArray[i], pos);
            int index = ++radixArrays[num][0];
            radixArrays[num][index] = pDataArray[I];
        }
 
        for (int i = 0, j =0; i < RADIX_10; i++) //写回到原数组中,复位radixArrays
        {
            for (int k = 1; k <= radixArrays[i][0]; k++)
                pDataArray[j++] = radixArrays[i][k];
            radixArrays[i][0] = 0;
        }
    }
}
/*基数排序*/
//1. 计数排序,按整形数值单位进行排序
void countSort(vector<int> &arr, int exp)
{
    int bucket[10] = { 0 };
    int arrSize = arr.size();
    int *pTemp = new int[arrSize];
    memset(pTemp, 0, arrSize * sizeof(int));

    //统计单位exp各数值计数值
    for (auto const val : arr)
        ++bucket[(val / exp) % 10];

    //计数分层
    for (int i = 1; i < 10; ++i)
        bucket[i] += bucket[i - 1];

    //按exp位大小用数组arr元素填充pTemp
    for (int i = arrSize - 1; i >= 0; --i)
        pTemp[ --bucket[(arr[i] / exp) % 10] ] = arr[i];
/*bugs*/
#if 0
    //bug1: bucket各层次的计数值没遍历一次相应自减1
    for (auto const val : arr)
        pTemp[bucket[(val / exp) % 10] - 1] = val;
    //bug2: arr数组元素每次排序时,下标应从大到小遍历,否则无法实现排序
    for (auto const val : arr)
        pTemp[ --bucket[(val / exp) % 10] ] = val;
#endif

    //pTemp -> arr
    for (int i = 0; i < arrSize; ++i)
        arr[i] = pTemp[i];
    delete []pTemp;
}
//2. 合并各单位计数排序结果
void radixSort(vector<int> &arr)
{
    int max = getMaxValue(arr);
    for (int exp = 1; max / exp != 0; exp *= 10)
        countSort(arr, exp);
}

2.2.3.12

Reference

[1]https://www.cnblogs.com/zhaoshuai1215/p/3448154.html

[2]https://www.cnblogs.com/KevinYZLong/p/5880151.html

  1. https://blog.csdn.net/hellozhxy/article/details/79911867
上一篇 下一篇

猜你喜欢

热点阅读