数构与算法:算法初步

一、什么是算法

关于算法, Hello 算法 是一个不错的参考网站

        算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间,空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。

特征:

  • 有穷性 (Finiteness)算法的有穷性是指算法必须能在执行有限个步骤之后终止;
  • 确切性 (Definiteness) 算法的每一步骤必须有确切的定义;
  • 输入项 (Input)   一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
  • 输出项 (Output) 一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
  • 可行性 (Effectiveness) 算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)。

(一)算法的描述

        算法的描述,直白而言是将算法的步骤表达出来。在不同的场景需求下,我们可以有不同的形式。

1、自然语言描述

        自然语言一般用于口语交流中,使用语言来详细地描述算法的步骤和逻辑。在文本中,可以通过列表的形式。

2、流程图

        是一种图形化的描述方法,使用图形符号来表示算法的执行过程和逻辑结构。在流程图中,可以使用不同的形状来表示算法中的各个步骤,并使用箭头来表示控制流程的转移。

源自: 什么是算法流程图?该怎么绘制?

更多可参考 

程序员 - 优秀的流程图应该是什么样的? - 个人文章 - SegmentFault 思否

3、伪代码

      伪代码是一种近似于编程语言的描述方式,用于描述算法的步骤和逻辑。它不是任何一种具体的编程语言,而是一种普通语言与编程语言的混合体,用于描述算法的思路和逻辑,忽略了具体的语法和细节。

        伪代码的目的是为了更加清晰地描述算法的逻辑,使读者能够理解算法的思路和过程,而不必关注具体的编程语言细节。

        伪代码通常使用一些常见的编程语言的关键字和符号,如if,else,while,for等,同时也可以使用一些自定义的关键字和符号来增强描述能力。

下面是一个使用伪代码描述冒泡排序算法的例子:

procedure bubbleSort(A: list of sortable items)
    n := length(A)
    repeat
        swapped := false
        for i := 0 to n-2 do
            if A[i] > A[i+1] then
                swap(A[i], A[i+1])
                swapped := true
            end if
        end for
    until not swapped
end procedure

        这段伪代码描述了冒泡排序算法的逻辑。在伪代码中,使用了一些常见的编程关键字,如procedure,repeat,for,if等,同时也使用了一些自定义的符号和操作,如:=表示赋值,>表示大于,swap表示交换元素等。

     

(二)算法的分析

        算法分析是对算法的执行时间和所需空间的估量,通常使用时间复杂度和空间复杂度来衡量。时间复杂度是指执行算法所需要的计算工作量,它反映了算法的执行速度。空间复杂度是指执行算法所需要的内存空间,它反映了算法的存储开销。

1、时间复杂度

        算法的时间复杂度主要描述了算法运行所需的时间长短。具体来说,它衡量了随着输入数据规模的增长,算法运行时间的增长趋势。

问题规模和语句频度

        不考虑计算机的软硬件等环境因素,影响算法时间代价的最主要因素是问题规模。问题规模是算法求解问题输入量的多少,是问题大小的本质表示,一般用整数n表示。问题规模n对不同的问题含义不同,例如,在排序运算中n为参加排序的记录数,在矩阵运算中n为矩阵的阶数,在多项式运算中n为多项式的项数,在集合运算中n为集合中元素的个数,在树的有关运算中n为树的节点个数,在图的有关运算中n为图的顶点数或边数。显然,n越大算法的执行时间越长。

        一个算法的执行时间大致上等于其所有语句执行时间的总和,而语句的执行时间则为该条语句的重复执行次数和执行一次所需时间的乘积。

        一条语句的重复执行次数称作语句频度(Frequency Count)

        由于语句的执行要由源程序经编译程序翻译成目标代码,目标代码经装配再执行,因此语句执行一次实际所需的具体时间是与机器的软、硬件环境(如机器速度、编译程序质量等)密切相关的。所以,所谓的算法分析并非精确统计算法实际执行所需时间,而是针对算法中语句的执行次数做出估计,从中得到算法执行时间的信息。设每条语句执行一次所需的时间均是单位时间,则一个算法的执行时间可用该算法中所有语句频度之和来度量。

计算方法

        为了方便表达数据结构和算法的时间复杂度,计算机科学家从数学界借鉴了一种简洁又通用的方式,那就是大 O 记法。这种规范化语言使得我们可以轻松地指出一个算法的性能级别,也令学术交流变得简单。

        大O表示法:算法的时间复杂度通常用大O符号表述,定义为T(n) = O(f(n))=O(n)。称函数T(n)以f(n)为界或者称T(n)受限于f(n)。  如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n)。T(n)称为这一算法的“时间复杂度”。当输入量n逐渐加大时,时间复杂度的极限情形称为算法的“渐近时间复杂度”。

例如,对于求1~n的整数和,n>1,一个C语言算法简单如下:

int i; // 语句频度1
int result; // 语句频度1
for(i = 1; i < n; i++){ // i = 1语句频度1; i < n 语句频度n; i++ 语句频度n+1
	result += i; // 语句频度n
}

此算法的时间复杂度f(n) = 3n+4

n趋向无穷大时,显然有

\lim_{0\rightarrow+\infty}f(n)/n = \lim_{n\rightarrow+\infty}(3n+4)/n = 3

即,f(n)n之比是一个不等于0的常数。即f(n)n是同阶的,或者说f(n)n

的数量级(Order of Magnitude)相同。在这里,我们用“O”来表示数量级,记作

T(n) = O(f(n))=O(n)

由此我们可以给出下述算法时间复杂度的定义。

        一般情况下,算法中基本语句重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作:

f(n)=a_mn^m + a_{m-1}n^{m-1}+...+a_1n+a^0

是一个m次多项式,则T(n)=O(n^m)

        它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度(Time Complexity)

        分析算法时间复杂度的基本方法为:找出所有语句中语句频度最大的那条语句作为基本语句,计算基本语句的频度得到问题规模n的某个函数f(n),取其数量级用符号“O”表示即可。

        在计算算法时间复杂度时,可以忽略所有低次幂项和最高次幂的系数,这样可以简化算法分析,也体现出了增长率的含义。

常见的时间复杂度量级
  • 常数时间复杂度:O(1)。这意味着无论输入规模如何,算法的运行时间都是恒定的。
  • 线性时间复杂度:O(n)。算法的运行时间与输入规模成正比。
  • 对数时间复杂度:O(log n)。算法的运行时间与输入规模的对数成正比。
  • 线性对数时间复杂度:O(n log n)。
  • 平方时间复杂度:O(n^2)。
  • 立方时间复杂度:O(n^3)。
  • 指数时间复杂度:O(2^n)。这是一个非常糟糕的情况,因为即使输入规模稍微增加一点,算法的运行时间也可能急剧增加。

2、空间复杂度

         空间复杂度是指算法在执行过程中所需的额外空间的量度,通常以字节为单位。它描述的是算法所需的额外空间随着问题规模的增长而变化的情况。

空间复杂度可以分为两类:

  1. 辅助空间复杂度:指算法在执行过程中所需的额外空间,不包括输入数据本身。例如,如果算法中使用了一个大小为n的数组来存储临时变量,那么辅助空间复杂度就为O(n)。

  2. 总空间复杂度:指算法在执行过程中所需的总的空间,包括辅助空间以及输入数据本身。通常情况下,总空间复杂度等于辅助空间复杂度加上输入数据的空间复杂度。例如,如果算法需要一个大小为n的数组来存储输入数据,并且还需要使用另外一个大小为m的数组来存储临时变量,那么总空间复杂度就为O(n + m)。

对于不同的算法,空间复杂度可能会有不同的度量方式,例如,对于排序算法,空间复杂度可以度量为算法所需的额外空间与输入数据的大小之和,即O(n),其中n为输入数据的大小。对于图算法,空间复杂度可以度量为算法所需的额外空间与图中顶点数和边数之和,即O(V + E),其中V为顶点数,E为边数。

在进行算法分析时,空间复杂度往往是一个重要的指标,因为它可以反映算法的空间利用率。较低的空间复杂度通常意味着算法使用的额外空间相对较少,节省了计算资源。然而,有时候为了提高算法的时间效率,可能需要牺牲一定的空间效率。因此,在算法设计时需要综合考虑时间复杂度和空间复杂度的平衡。

二、线性搜索算法

      这里的线性,指的是,搜索的维度是一维的,即在诸如数组中的搜索,对于每一个当前搜索元素,有且仅有向前或向后搜索的可能。

(一)顺序查找

        对于无序,或者完全没有其它辅助信息可以帮助我们略过某个线性序列而找到我们的目标,顺序查找是唯一的方法,它是一种简单直观的搜索算法,用于在一个列表或数组中查找目标元素。

        搜索算法从列表的第一个元素开始,逐个地比较每个元素,直到找到目标元素或搜索完整个列表。

        顺序搜索算法的时间复杂度为O(n),其中n是列表的长度。这是因为在最坏情况下,需要遍历整个列表才能找到目标元素。因此,顺序搜索算法在大型列表中的效率相对较低。

        然而,顺序搜索算法的优点是它的实现简单,无需对列表进行排序。因此,在小型列表或无序列表中,顺序搜索算法是一种有效的搜索方法。

(二)折半查找

        折半查找,也叫二分查找,是一种在有序数组中查找特定元素的算法。它的思想是将数组中间位置的元素与目标元素进行比较,根据比较结果可以确定目标元素在数组的左半部分或右半部分。然后再在相应的半部分中继续查找,直到找到目标元素或者确定目标元素不存在为止。

具体步骤如下:

  1. 确定目标元素要查找的范围,通常是整个数组。设定左边界和右边界,左边界为0,右边界为数组长度减1。
  2. 计算中间位置的索引,可以使用 (左边界 + 右边界) / 2 的方式计算。如果数组长度是奇数,则取中间位置索引为整数结果;如果数组长度是偶数,则取中间两个位置中靠左的那个索引。
  3. 比较中间位置的元素与目标元素的大小。如果中间位置的元素等于目标元素,则查找成功,返回中间位置的索引。如果中间位置的元素大于目标元素,则新的右边界更新为中间位置的索引减1。如果中间位置的元素小于目标元素,则新的左边界更新为中间位置的索引加1。
  4. 在新的范围内重复步骤2和步骤3,直到找到目标元素或者确定目标元素不存在为止。如果左边界大于右边界,则表示目标元素不存在。

        折半查找的时间复杂度为O(log n),其中n表示数组的长度。由于每次查找都能将查找范围缩小一半,因此查找效率非常高。但是要求数组必须是有序的,否则折半查找无法正确运行。另外,折半查找只能在静态数组或者静态链表中使用,因为要求可以根据索引访问元素。如果是动态数组或者链表,需要先将其转换为静态形式才能进行折半查找。

(三)插值查找

插值查找是一种在已排序的数组中搜索元素的算法。和二分查找类似,插值查找也是利用了数组有序的特点来进行查找。

插值查找的原理是根据要查找的元素在数组中的分布情况,通过插值来预测要查找的元素可能在的位置。具体步骤如下:

  1. 计算要查找的元素与数组第一个元素的差值,并与数组第一个元素到最后一个元素的差值的比例相乘,得到一个比例因子。
  2. 将比例因子与数组的长度相乘,得到一个预测的位置。
  3. 如果预测的位置上的元素等于要查找的元素,则找到了目标元素;如果预测的位置上的元素大于要查找的元素,则继续在数组的前半部分进行插值查找;如果预测的位置上的元素小于要查找的元素,则继续在数组的后半部分进行插值查找。
  4. 重复上述步骤,直到找到目标元素或者确定目标元素不存在。

插值查找的时间复杂度为O(logn),其中n为数组的长度。插值查找在元素分布比较均匀的情况下,效果较好;但在元素分布不均匀的情况下,插值查找的效果不一定比二分查找好。

三、线性排序算法

        所谓排序算法,即通过特定的算法因式将一组或多组数据按照既定模式进行重新排序。这种新序列遵循着一定的规则,体现出一定的规律,因此,经处理后的数据便于筛选和计算,大大提高了计算效率。

        对于排序,我们首先要求其具有一定的稳定性,即当两个相同的元素同时出现于某个序列之中,则经过一定的排序算法之后,两者在排序前后的相对位置不发生变化。换言之,即便是两个完全相同的元素,它们在排序过程中也是各有区别的,不允许混淆不清。

 (一)选择排序

        选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法步骤
  1. 从数列中选出最小(大)元素,存放到序列的起始位置;
  2. 遍历剩余数列,重复步骤1,直到排序完整个数列。

选择排序的时间复杂度为O(n²),因为它需要遍历数列来查找每个位置上的最小(大)元素。

C语言实现 
void selectionSort(int arr[], int n) {
    int i, j, min_idx;
    for (i = 0; i < n-1; i++) {
        // 找到最小元素的索引
        min_idx = i;
        for (j = i+1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
        // 将找到的最小元素交换到未排序数组的开头
        int temp = arr[min_idx];
        arr[min_idx] = arr[i];
        arr[i] = temp;
    }
}

(二)插入排序

        插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常使用in-place排序(即只需用到O(1)的额外空间的排序)。

算法步骤
  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

C语言实现 
void insertionSort(int arr[], int n) {
    int i, j, key;
    for (i = 1; i < n; i++) {
        key = arr[i];
        j = i - 1;
        // 移动元素,直到找到合适的插入位置
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
}

(三)希尔排序

        希尔排序(Shell Sort)是一种基于插入排序的算法,但它通过比较相距一定间隔的元素来工作,所以也被称为“缩小增量排序”。希尔排序是不稳定的排序算法,它通过比较距离较远的元素来减少数据移动的次数,从而提高排序效率。

算法步骤
  1. 选择一个增量序列t1,t2,…,tk,其中ti > tj,对于所有i < j。
  2. 按增量序列个数k,对序列进行k轮排序。
  3. 每轮排序根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各个子序列进行直接插入排序。
  4. 所有子序列排序完成后,再整体进行一次直接插入排序。

希尔排序的时间复杂度取决于增量序列的选择,平均时间复杂度为O(n log n)。

C语言实现 
void shellSort(int arr[], int n) {
    for (int gap = n / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < n; i++) {
            int temp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
}

(四)冒泡排序

        冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行,直到没有再需要交换的元素为止。

算法步骤
  1. 比较相邻的元素。如果第一个比第二个大(升序排序),就交换它们两个;
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后已经排序好的元素;
  4. 重复步骤1~3,直到排序完成。

 

C语言实现  
void bubbleSort(int arr[], int n)
{
    int i, j;
    int isSwap = 1; // 标志位,是否交换过
    for(i = 0; i < n-1 && isSwap; i++) // 进行 n-1 轮冒泡
    {
        isSwap = 0;
        for(j = 0; j < n-i-1; j++) // 每一轮冒泡比较的次数逐渐减少
        {
            if(arr[j] > arr[j+1]) // 如果相邻两个数的顺序错误,则交换它们的位置
            {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                isSwap = 1;
            }
        }
    }
}

 (五)快速排序

        快速排序(Quick Sort)是由东尼·霍尔所发展的一种排序算法,是一种分治算法。它的工作原理是通过选取一个“基准”元素,将数组分为两部分,一部分都比基准小,另一部分都比基准大,然后递归地对这两部分继续进行快速排序。

算法步骤
  1. 选择一个基准元素;
  2. 将比基准小的元素移到基准前面,将比基准大的元素移到基准后面;
  3. 对基准前面的子数组和基准后面的子数组递归地执行快速排序。
C语言实现 
// 交换两个元素的值
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 找到基准元素的正确位置,并返回该位置的索引
int partition(int arr[], int low, int high) {
    int pivot = arr[high];  // 选择最后一个元素作为基准
    int i = (low - 1);  // 指向小于基准的元素

    for (int j = low; j <= high - 1; j++) {
        // 如果当前元素小于等于基准,进行交换
        if (arr[j] <= pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return (i + 1);
}

// 快速排序函数
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pivotIndex = partition(arr, low, high);  // 基准元素的位置
        quickSort(arr, low, pivotIndex - 1);  // 对基准元素左边的子数组进行排序
        quickSort(arr, pivotIndex + 1, high);  // 对基准元素右边的子数组进行排序
    }
}


(六)归并排序

归并排序(Merge Sort)是一种分治算法,其工作原理是将数组分为若干个子数组,先使每个子数组有序,然后再使子数组之间有序。归并排序的步骤可以概括为“分解、解决、合并”。

算法步骤
  1. 分解:将原始数组分解为若干个子数组,直到每个子数组只包含一个元素;
  2. 解决:对每个子数组进行排序(已经是一个元素,自然有序);
  3. 合并:将已排序的子数组合并为整体有序的数组。
C语言实现  
void merge(int arr[], int l, int m, int r) {
    int i, j, k;
    int n1 = m - l + 1; // 左子数组的长度
    int n2 = r - m; // 右子数组的长度
    // 创建临时数组
    int L[n1], R[n2];
    // 复制数据到临时数组
    for (i = 0; i < n1; i++)
        L[i] = arr[l + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[m + 1 + j];
    // 合并临时数组回到原数组
    i = 0; // 初始化左子数组的索引
    j = 0; // 初始化右子数组的索引
    k = l; // 初始化合并回原数组的索引
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    // 复制剩余的左子数组元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    // 复制剩余的右子数组元素
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}
void mergeSort(int arr[], int l, int r) {
    if (l < r) {
        // 找到中间点
        int m = l + (r - l) / 2;
        // 递归地对左半边和右半边进行归并排序
        mergeSort(arr, l, m);
        mergeSort(arr, m + 1, r);
        // 合并两个有序的半边
        merge(arr, l, m, r);
    }
}

(七)堆排序

堆排序(Heap Sort)是一种基于比较的排序算法,它利用堆这种数据结构的特性来进行排序。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

算法步骤
  1. 构建一个最大堆;
  2. 将堆顶元素(最大值)与堆的最后一个元素交换,然后减少堆的大小;
  3. 破坏堆的性质,调整堆,使其满足最大堆的性质;
  4. 重复步骤2和3,直到堆的大小为1。
C语言实现
void heapify(int arr[], int n, int i) {
    int largest = i; // 初始化最大值为根节点
    int l = 2 * i + 1; // 左子节点
    int r = 2 * i + 2; // 右子节点
    // 如果左子节点比根节点大
    if (l < n && arr[l] > arr[largest])
        largest = l;
    // 如果右子节点比最大值大
    if (r < n && arr[r] > arr[largest])
        largest = r;
    // 如果最大值不是根节点
    if (largest != i) {
        int swap = arr[i];
        arr[i] = arr[largest];
        arr[largest] = swap;
        // 递归地调整受影响的子树
        heapify(arr, n, largest);
    }
}
void heapSort(int arr[], int n) {
    // 构建最大堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);
    // 一个个从堆顶取出元素
    for (int i = n - 1; i > 0; i--) {
        // 当前根移到末尾
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        // 调整堆
        heapify(arr, i, 0);
    }
}

 (八)基数排序

        基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数的每一位都可能是0-9中的一个数字,基数排序会进行最多10轮的比较排序(如果考虑负数,则需要更多轮)。

算法步骤
  1. 确定输入数组中最大数和最小数的位数。
  2. 从最低位开始,对每一位进行计数排序(利用计数排序对每一位进行排序)。
  3. 每一位排序完成后,将排序好的数组合并起来。
C语言实现  
void countingSort(int arr[], int n, int exp) {
    int output[n];
    int i;
    int count[10] = {0}; // 计数器,用于存储每个数字的出现次数
    // 统计每个数字的出现次数
    for (i = 0; i < n; i++) {
        count[(arr[i] / exp) % 10]++;
    }
    // 计算每个数字的起始位置
    for (i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }
    // 输出排序结果
    for (i = n - 1; i >= 0; i--) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }
    // 复制排序后的数组到原数组
    for (i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}
void radixSort(int arr[], int n) {
    // 找到最大数的位数
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    int exp = 1; // 从个位数开始排序
    while (max / exp > 0) {
        countingSort(arr, n, exp);
        exp *= 10;
    }
}

 (九)计数排序

        计数排序(Counting Sort)是一种非比较型整数排序算法,其原理是将输入序列中的每个元素值映射到一段固定长度的数组上,数组中的每个位置记录了该值出现的次数。通过这个计数数组,我们可以快速得出每个值在排序后的位置。

算法步骤
  1. 确定输入数组的最大值和最小值,以确定计数数组的大小。
  2. 创建一个计数数组,用于记录每个值出现的次数。
  3. 遍历输入数组,统计每个值出现的次数。
  4. 遍历计数数组,根据计数值将元素输出到结果数组中。
 
C语言实现 
void countingSort(int arr[], int n, int maxValue) {
    int i;
    // 创建计数数组
    int *count = (int *)malloc((maxValue + 1) * sizeof(int));
    for (i = 0; i <= maxValue; i++) {
        count[i] = 0;
    }
    // 统计每个值出现的次数
    for (i = 0; i < n; i++) {
        count[arr[i]]++;
    }
    // 生成排序结果
    i = 0;
    for (int j = 0; j <= maxValue; j++) {
        while (count[j] > 0) {
            arr[i] = j;
            count[j]--;
            i++;
        }
    }
    free(count); // 释放计数数组内存
}

(十)桶排序

         桶排序(Bucket Sort)是一种非比较型整数排序算法,其原理是将待排序的数据分到几个有序的桶里,每个桶里的数据再分别排序。适用于数据分布均匀且范围不大的情况。

        是计数排序的升级版。它将元素分到有限数量的桶里,每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

算法步骤
  1. 确定数据的最大值和最小值,并计算出桶的数量和每个桶的宽度。
  2. 将数据分配到各个桶中,每个桶内进行排序(通常使用插入排序)。
  3. 依次从桶中取出排序好的数据,合并得到最终排序结果。 
C语言实现 
void bucketSort(int arr[], int n, int bucketSize) {
    // 找到最大最小值
    int minValue = arr[0];
    int maxValue = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < minValue) {
            minValue = arr[i];
        } else if (arr[i] > maxValue) {
            maxValue = arr[i];
        }
    }
    // 计算桶的数量和宽度
    int bucketCount = (maxValue - minValue) / bucketSize + 1;
    int *buckets[bucketCount];
    for (int i = 0; i < bucketCount; i++) {
        buckets[i] = (int *)malloc(bucketSize * sizeof(int));
    }
    // 分配数据到桶中
    for (int i = 0; i < n; i++) {
        int index = (arr[i] - minValue) / bucketSize;
        buckets[index][arr[i] % bucketSize] = arr[i];
    }
    // 桶内排序(这里使用了插入排序)
    for (int i = 0; i < bucketCount; i++) {
        insertionSort(buckets[i], bucketSize);
    }
    // 合并桶中的数据
    int index = 0;
    for (int i = 0; i < bucketCount; i++) {
        for (int j = 0; j < bucketSize; j++) {
            if (buckets[i][j] != NULL) {
                arr[index++] = buckets[i][j];
            }
        }
    }
    // 释放桶内存
    for (int i = 0; i < bucketCount; i++) {
        free(buckets[i]);
    }
}
void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值