数据结构和算法——用C语言实现所有排序算法

news2024/9/22 1:41:13

文章目录

  • 前言
  • 排序算法的基本概念
  • 内部排序
    • 插入排序
      • 直接插入排序
      • 折半插入排序
      • 希尔排序
    • 交换排序
      • 冒泡排序
      • 快速排序
    • 选择排序
      • 简单选择排序
      • 堆排序
    • 归并排序
    • 基数排序
  • 外部排序
    • 多路归并
    • 败者树
    • 置换——选择排序
    • 最佳归并树

前言

本文所有代码均在仓库中,这是一个完整的由纯C语言实现的可以存储任意类型元素的数据结构的工程项目。

在这里插入图片描述

  • 首先是极好的工程意识,该项目是一个中大型的CMake项目,结构目录清晰,通过这个项目可以遇见许多工程问题并且可以培养自己的工程意识。
  • 其次是优秀的封装性(每个数据结构的头文件中只暴漏少量的信息),以及优秀的代码风格和全面的注释,通过这个项目可以提升自己的封装技巧:

在这里插入图片描述

  • 异常处理功能:在使用C语言编写代码的时候不能使用类似Java的异常处理机制是非常难受的,所以我也简单实现了一下。详情可看在C语言中实现类似面向对象语言的异常处理机制

在这里插入图片描述

最后也是最重要的一点,数据结构的通用性和舒适的体验感,下面以平衡二叉树为例:

  • 第一步:要想使用平衡二叉树,只需要引入其的头文件:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
  • 第二步:定义自己任意类型的数据,并构造插入数据(以一个自定义的结构体为例):
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"

int dataCompare(void *, void *);

typedef struct People {
    char *name;
    int age;
} *People;

int main(int argc, char **argv) {
    struct People dataList[] = {
            {"张三", 15},
            {"李四", 3},
            {"王五", 7},
            {"赵六", 10},
            {"田七", 9},
            {"周八", 8},
    };
    BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
    for (int i = 0; i < 6; ++i) {
        balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
    }
    return 0;
}

/**
 * 根据人的年龄比较
 */
int dataCompare(void *data1, void *data2) {
    int sub = ((People) data1)->age - ((People) data2)->age;
    if (sub > 0) {
        return 1;
    } else if (sub < 0) {
        return -1;
    } else {
        return 0;
    }
}
  • 第三步:打印一下平衡二叉树:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"

int dataCompare(void *, void *);

void dataPrint(void *);

typedef struct People {
    char *name;
    int age;
} *People;

int main(int argc, char **argv) {
    struct People dataList[] = {
            {"张三", 15},
            {"李四", 3},
            {"王五", 7},
            {"赵六", 10},
            {"田七", 9},
            {"周八", 8},
    };
    BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
    for (int i = 0; i < 6; ++i) {
        balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
        balancedBinaryTreePrint(tree, dataPrint);
        printf("-------------\n");
    }
    return 0;
}

/**
 * 根据人的年龄比较
 */
int dataCompare(void *data1, void *data2) {
    int sub = ((People) data1)->age - ((People) data2)->age;
    if (sub > 0) {
        return 1;
    } else if (sub < 0) {
        return -1;
    } else {
        return 0;
    }
}

/**
 * 打印人的年龄
 * @param data
 */
void dataPrint(void *data) {
    People people = (People) data;
    printf("%d", people->age);
}

打印的结果如下:

在这里插入图片描述
最后期待大佬们的点赞。

排序算法的基本概念

排序算法就是将结构中所有数据按照关键字有序的过程。排序的分类如下:

在这里插入图片描述

评价一个排序算法的指标通常有以下三种:

  • 时间复杂度
  • 空间复杂度
  • 稳定性

其中稳定性是指关键字相同的元素在排序前后相对位置是否改变,如果不变则称该排序算法是稳定的,否则就是不稳定的。

内部排序

插入排序

算法思想:是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。

直接插入排序

  • 算法思想:边寻找无序元素插入的位置边向后移动有序序列。
  • 时间复杂度: O ( n ) ∼ O ( n 2 ) O(n)\thicksim O(n²) O(n)O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
/**
 * 直接插入排序
 * @param dataList
 * @param length
 */
void directInsert(void *dataList[], int length, int (*compare)(void *, void *)) {
    for (int i = 2; i <= length; ++i) {
        void *data = dataList[i];
        int j;
        for (j = i - 1; j > 0 && compare(data, dataList[j - 1]) < 0; --j) {
            dataList[j + 1 - 1] = dataList[j - 1];
        }
        dataList[j + 1 - 1] = data;
    }
}

折半插入排序

  • 算法思想:先用二分查找寻找无序元素的位置再向后移动有序序列。
  • 时间复杂度: O ( n l o g 2 n ) ∼ O ( n 2 ) O(nlog₂n)\thicksim O(n²) O(nlog2n)O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
/**
 * 折半插入排序
 * @param dataList
 * @param length
 */
void binaryInsertSort(void *dataList[], int length, int (*compare)(void *, void *)) {
    for (int i = 2; i <= length; ++i) {
        void *data = dataList[i];
        int mid, high = i - 1, low = 1;
        while (low <= high) {
            mid = (high + low) / 2;
            if (compare(dataList[mid - 1], data) > 0) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        for (int j = i; j > low; j--) {
            dataList[j - 1] = dataList[j - 1 - 1];
        }
        dataList[low - 1] = data;
    }
}

希尔排序

  • 算法思想:先将待排序列表分割成若干形如 L [ i , i + d , i + 2 d , … , i + k d ] L[i,i+d,i+2d,\dots,i+kd] L[i,i+d,i+2d,,i+kd]的子表,然后对各个子表分别进行直接插入排序,之后缩小增量 d d d,重复上述过程,直到 d = 1 d=1 d=1
  • 时间复杂度:无法用数学方法准确表示,当 n n n在某一范围内时间复杂度为 O ( n 1.3 ) O(n^{1.3}) O(n1.3),最坏的时间复杂度为 O ( n 2 ) O(n²) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定
/**
 * 希尔排序
 * @param dataList
 * @param length
 * @param compare
 */
void shellSort(void *dataList[], int length, int (*compare)(void *, void *)) {
    for (int p = length / 2; p >= 1; p /= 2) {
        for (int i = p + 1; i <= length; ++i) {
            void *data = dataList[i - 1];
            int j;
            for (j = i - p; j > 0 && compare(data, dataList[j - 1]) < 0; j -= p) {
                dataList[j + p - 1] = dataList[j - 1];
            }
            dataList[j + p - 1] = dataList[j - 1];
        }
    }
}

交换排序

算法思想:根据序列中两个元素关键字的比较结果来对换这两个元素在序列中的位置。

冒泡排序

  • 算法思想:从前往后或从后往前两两比较相邻两元素的关键字,若为逆序则交换它们,直到序列比较完。
  • 时间复杂度: O ( n ) ∼ O ( n 2 ) O(n)\thicksim O(n²) O(n)O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
/**
 * 冒泡排序
 * @param dataList
 * @param length
 * @param compare
 */
void bubbleSort(void *dataList[], int length, int (*compare)(void *, void *)) {
    for (int i = 1; i <= length - 1; i++) {
        bool flag = false;
        for (int j = length; j > i; j--) {
            if (compare(dataList[j - 1], dataList[j - 1 - 1]) < 0) {
                swap(dataList + j - 1, dataList + j - 1 - 1);
                flag = true;
            }
        }
        if (!flag) {
            break;
        }
    }
}

快速排序

快速排序算法的平均时间复杂度接近最好时间复杂度,是最好的内部排序。

  • 算法思想:在待排序列中选择一个元素 p i v o t pivot pivot作为基准,通过一趟排序将序列划分为两部分 L [ 1 , … , k − 1 ] L[1,\dots,k-1] L[1,,k1] L [ k + 1 , … , n ] L[k+1,\dots,n] L[k+1,,n],使得 L [ 1 , … , k − 1 ] L[1,\dots,k-1] L[1,,k1]中所有元素小于 p i v o t pivot pivot L [ k + 1 , … , n ] L[k+1,\dots,n] L[k+1,,n]中所有元素大于等于 p i v o t pivot pivot p i o v t piovt piovt则放在了其最终的位置 L [ k ] L[k] L[k]上,这个过程为一趟快速排序。然后分别递归的对两个部分重复上述过程,直到每部分只有一个元素或空为止。
  • 时间复杂度: O ( n l o g 2 n ) ∼ O ( n 2 ) O(nlog₂n)\thicksim O(n²) O(nlog2n)O(n2),具体为 O ( n × 递归层数 ) O(n\times 递归层数) O(n×递归层数)
  • 空间复杂度: O ( l o g 2 n ) ∼ O ( n ) O(log₂n)\thicksim O(n) O(log2n)O(n),具体为 O ( 递归层数 ) O(递归层数) O(递归层数)
  • 稳定性:不稳定
static int partition(void *dataList[], int low, int high, int (*compare)(void *, void *)) {
    void *pivot = dataList[low - 1];
    while (low < high) {
        while (low < high && compare(dataList[high - 1], pivot) > 0) {
            high--;
        }
        dataList[low - 1] = dataList[high - 1];
        while (low < high && compare(dataList[low - 1], pivot) <= 0) {
            low++;
        }
        dataList[high - 1] = dataList[low - 1];
    }
    dataList[low - 1] = pivot;
    return low;
}

/**
 * 快速排序
 * @param dataList 
 * @param low 
 * @param high 
 * @param compare 
 */
void quickSort(void *dataList[], int low, int high, int (*compare)(void *, void *)) {
    if (low < high) {
        int pivotPos = partition(dataList, low, high, compare);
        quickSort(dataList, low, pivotPos - 1, compare);
        quickSort(dataList, pivotPos + 1, high, compare);
    }
}

选择排序

算法思想:每一趟在待排序元素中选择关键字最小或最大的元素加入有序子序列。

简单选择排序

  • 算法思想:第 i i i趟从 L ( i . . . n ) L(i...n) L(i...n)中选择关键字最小的元素与 L ( i ) L(i) L(i)交换,每一趟排序都可以确定一个元素的最终位置。
  • 时间复杂度: O ( n 2 ) O(n²) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定
/**
 * 简单选择排序
 * @param dataList 
 * @param length 
 * @param compare 
 */
void SimpleSelectSort(void *dataList[], int length, int (*compare)(void *, void *)) {
    for (int i = 1; i < length; ++i) {
        int minIndex = i;
        for (int j = i + 1; j <= length; ++j) {
            if (compare(dataList[j], dataList[minIndex]) < 0) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            swap(dataList[i], dataList[minIndex]);
        }
    }
}

堆排序

当一个序列 L [ 1 , … , n ] L[1,\dots,n] L[1,,n]满足:

  • L ( i ) > = L ( 2 i ) L(i)>=L(2i) L(i)>=L(2i) L ( i ) > = L ( 2 i + 1 ) L(i)>=L(2i+1) L(i)>=L(2i+1)时,称该序列为大顶堆
  • L ( i ) < = L ( 2 i ) L(i)<=L(2i) L(i)<=L(2i) L ( i ) < = L ( 2 i + 1 ) L(i)<=L(2i+1) L(i)<=L(2i+1)时,称该序列为小顶堆

可以将堆看成一棵线性存储的完全二叉树:

  • 大顶堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点的值。
  • 小顶堆的最小元素存放在根结点,且其任一非根结点的值大于等于其双亲结点的值。
  • 在完全二叉树中:
    • i < = ⌊ n / 2 ⌋ i<=⌊n/2⌋ i<=n/2,那么结点 i i i为分支结点,否则为叶子结点。
    • i i i的左孩子 2 i 2i 2i
    • i i i的右孩子 2 i + 1 2i+1 2i+1
    • i i i的父结点 ⌊ i / 2 ⌋ ⌊i/2⌋ i/2

堆排序首要任务就是先构建一个堆(以大顶堆为例):

  • 检查所有分支结点的关键字是否满足大顶堆的性质,如果不满足,则用最大孩子的关键字和分支结点的关键字交换,使该分支子树成为大顶堆。之后依次对 ⌊ n / 2 ⌋ − 1 ∼ 1 ⌊n/2⌋-1\thicksim1 n/211位置的分支结点重复以上检查。
  • 若关键字交换破坏了下一级的堆,则采用相同的方式继续往下调整。

堆构建完后就可以进行堆排序了,堆排序的算法思想如下:

  • 每一趟将堆顶元素加入有序子序列(与待排序列中的最后一个元素交换),并将待排元素序列再次调整为大顶堆。
  • 时间复杂度:
    • 建立堆时: O ( n ) O(n) O(n)
    • 排序时: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
    • 整体: O ( n l o g 2 n ) O(nlog₂n) O(nlog2n)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定

如果要在堆中插入或删除元素(以小顶堆为例),那么思想为:

  • 插入元素时,首先将新元素放到堆尾,然后与父结点对比,若新元素比父结点更小,则将两者互换,一直重复此步骤直至新元素无法上升。
  • 删除元素时,首先用堆底元素代替被删除的元素,然后让该元素不断的下坠,直到无法下坠为止。
static void heapAdjust(void **dataList, int rootIndex, int length, int (*compare)(void *, void *)) {
    void *root = dataList[rootIndex - 1];
    //i指向左孩子
    for (int i = 2 * rootIndex; i <= length; i *= 2) {
        //如果右孩子>左孩子,则让i指向右孩子
        if (i < length && compare(dataList[i + 1 - 1], dataList[i - 1]) > 0) {
            i++;
        }
        if (compare(root, dataList[i - 1]) > 0) {
            break;
        } else {
            dataList[rootIndex - 1] = dataList[i - 1];
            rootIndex = i;
        }
    }
    dataList[rootIndex - 1] = root;
}

static void buildMaxHeap(void **dataList, int length, int (*compare)(void *, void *)) {
    for (int i = length / 2; i > 0; i--) {
        heapAdjust(dataList, i, length, compare);
    }
}

/**
 * 堆排序
 * @param dataList
 * @param length
 * @param compare
 */
void heapSort(void **dataList, int length, int (*compare)(void *, void *)) {
    buildMaxHeap(dataList, length, compare);
    for (int i = length; i > 1; i--) {
        swap(dataList[i - 1], dataList[1 - 1]);
        heapAdjust(dataList, 1 - 1, i - 1, compare);
    }
}

归并排序

  • 算法思想:将待排序列视为 n n n个有序的子序列,然后两两(或两个以上)归并,得到 ⌈ n / 2 ⌉ ⌈n/2⌉ n/2个长度为 2 2 2或为 1 1 1的有序序列,然后继续归并,直到合成一个长度为 n n n的有序序列为止。
  • 时间复杂度: O ( n l o g 2 n ) O(nlog₂n) O(nlog2n)
  • 空间复杂度: O ( n ) O(n) O(n)
  • 稳定性:稳定
static void merge(void *dataList[], int length, int low, int mid, int high, int (*compare)(void *, void *)) {
    void *temp[length];
    int i, j, k;
    for (k = low; k <= high; ++k) {
        temp[k] = dataList[k];
    }
    for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
        if (compare(temp[i - 1], temp[j - 1]) < 0) {
            dataList[k - 1] = temp[i - 1];
            i--;
        } else {
            dataList[k - 1] = temp[j - 1];
            j++;
        }
    }
    while (i <= mid) {
        dataList[k - 1] = temp[i - 1];
        k++;
        i++;
    }
    for (; j <= high;) {
        dataList[k - 1] = temp[j - 1];
        k++;
        j++;
    }
}

/**
 * 归并排序
 * @param dataList 
 * @param length 
 * @param low 
 * @param high 
 * @param compare 
 */
void mergeSort(void *dataList[], int length, int low, int high, int (*compare)(void *, void *)) {
    if (low < high) {
        int mid = (low + high) / 2;
        mergeSort(dataList, length, low, mid, compare);
        mergeSort(dataList, length, mid + 1, high, compare);
        merge(dataList, length, low, mid, high, compare);
    }
}

基数排序

假设长度为 n n n的排序列表中每个结点 a j a_j aj的关键字由 d d d元组 ( k j d − 1 , k j d − 2 , … , k j 1 , k j 0 ) (k_j^{d-1},k_j^{d-2},\dots,k_j^1,k_j^0) (kjd1,kjd2,,kj1,kj0)组成,其中 0 ≤ k j i ≤ r − 1 , ( 0 ≤ j < n , 0 ≤ i ≤ d − 1 ) 0\leq k_j^i\leq r-1,(0\leq j<n,0\leq i\leq d-1) 0kjir1,(0j<n,0id1) r r r称为基数。那么基数排序的算法思想为:

  • 初始化:设置 r r r个空队列, Q 0 , Q 1 , … , Q r − 1 Q_0,Q_1,\dots,Q_{r-1} Q0,Q1,,Qr1
  • 按照各个关键字位权重递增的次数对 d d d个关键字位分别进行分配和收集
    • 分配:顺序扫描各个元素,若当前处理的关键字位 = x =x =x,则将元素插入 Q x Q_x Qx队尾
    • 收集:把 Q 0 , Q 1 , … , Q r − 1 Q_0,Q_1,\dots,Q_{r-1} Q0,Q1,,Qr1各个队列中的结点依次出队链接
  • 时间复杂度: O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))
  • 空间复杂度: O ( r ) O(r) O(r)
  • 稳定性:稳定

int dataList[] = {278, 109, 63, 930, 589, 184, 505, 269, 8, 83}为例:

/**
 * 基数排序
 * @param dataList
 * @param length
 * @param maxLength
 */
void radixSort(int dataList[], int length, int maxLength) {
    LinkedQueue queue = linkedQueueConstructor();
    for (int i = 0; i < length; ++i) {
        linkedQueueEnQueue(queue, dataList + i);
    }
    LinkedQueue queue0 = linkedQueueConstructor();
    LinkedQueue queue1 = linkedQueueConstructor();
    LinkedQueue queue2 = linkedQueueConstructor();
    LinkedQueue queue3 = linkedQueueConstructor();
    LinkedQueue queue4 = linkedQueueConstructor();
    LinkedQueue queue5 = linkedQueueConstructor();
    LinkedQueue queue6 = linkedQueueConstructor();
    LinkedQueue queue7 = linkedQueueConstructor();
    LinkedQueue queue8 = linkedQueueConstructor();
    LinkedQueue queue9 = linkedQueueConstructor();
    LinkedQueue queueList[] = {queue0, queue1, queue2, queue3, queue4, queue5, queue6, queue7, queue8, queue9};
    for (int i = 1; i <= maxLength; ++i) {
        while (!linkedQueueIsEmpty(queue)) {
            void *data = linkedQueueDeQueue(queue);
            int key = *(int *) data / (int) pow(10, i - 1) % 10;
            linkedQueueEnQueue(queueList[key], data);
        }
        for (int j = 0; j < 10; ++j) {
            LinkedQueue keyQueue = queueList[j];
            while (!linkedQueueIsEmpty(keyQueue)) {
                linkedQueueEnQueue(queue, linkedQueueDeQueue(keyQueue));
            }
        }
    }
    while (!linkedQueueIsEmpty(queue)) {
        void *data = linkedQueueDeQueue(queue);
        printf("%d,", *(int *) data);
    }
}

在这里插入图片描述

外部排序

多路归并

操作系统以为单位对磁盘存储空间进行管理,如果要修改磁盘块中的数据,就需要把对应磁盘块的内容读到内存中,在内存中修改后再写回磁盘。在对磁盘数据进行排序时,如果磁盘中的数据过多,那么无法一次将数据全部读到内存中,此时就应该使用外部排序。实现外部排序的思想是使用归并排序的的方法,最少只需要在内存中分配三块大小的缓冲区即可对任意一个大文件进行排序。

在这里插入图片描述
外部排序的步骤如下:

  • 构造归并段:每次将磁盘中两个块的内容读入输入缓冲区中,进行内部排序写到输出缓冲区,当某个输入缓冲区为空时就立即读入磁盘中的下一个段,当输出缓冲区已满时就写入到磁盘中。16个块都排序完后就构造了8个两块长度的初始归并段。
  • 接着继续构造4个4块长度的归并段。
  • 以此类推当只有一个归并段时整个磁盘就变得有序了。

在每次构造归并段时都需要把所有的磁盘块读写一遍,并且还要进行内部排序,因此外部排序的时间开销由以下几部分构成:
外部排序的时间开销 = 读写外存的时间 + 内部排序所需时间 + 内部归并所需的时间 外部排序的时间开销=读写外存的时间+内部排序所需时间+内部归并所需的时间 外部排序的时间开销=读写外存的时间+内部排序所需时间+内部归并所需的时间
其中读写外存的时间是外部排序的主要开销,因此可以使用多路归并的方式来减少归并的趟数从而减少读写外存的次数。若对 r r r个初始归并段做 k k k路归并,则归并树可用 k k k叉树表示,若树高为 h h h,则归并趟数 n n n为:
n = h − 1 = ⌈ l o g k r ⌉ n=h-1=⌈log_kr⌉ n=h1=logkr
因此归并路数(增加缓冲区的个数)越多,初始归并段(增加缓冲区的长度)越少,读写磁盘的次数就越少。但多路归并同样存在着问题:

  • 问题一: k k k路归并时,需要开辟 k k k个输入缓冲区,内存开销增大。
  • 问题二:每挑选一个关键字需要对比关键字 k − 1 k-1 k1次,内部归并所需要的时间增加。

败者树

败者树可视为多一个根结点的完全二叉树, k k k个叶结点分别是当前参加比较的元素,非叶子结点用来记忆左右子树中的失败者,而让胜者往上继续进行比较,一直到根结点。

在这里插入图片描述

可以将败者树用于多路归并从而减少关键字的对比,从而解决问题二。对于 k k k路归并,第一次构造败者树需要对比关键字 k − 1 k-1 k1次,有了败者树,选出最小元素,只需要对比关键字 ⌈ l o g 2 k ⌉ ⌈log_2k⌉ log2k次。

置换——选择排序

对于传统归并段构造的方法,如果用于内部排序的输入缓冲区可容纳 l l l个记录,则每个初始归并段也只能包含 l l l个记录,若文件共有 n n n个记录,则初始归并段的数量为 r = n l r=\frac{n}{l} r=ln。而置换——选择排序可以构造比内存缓冲区长度长的归并段。置换——选择排序的思想为:

  • 假设输入缓冲区的大小为三,读入代排文件中的三个记录。
  • 每次将缓冲区的最小记录放到归并段一的末尾(并在内存中记录这个最小记录miniMax),接着读入待排文件的下一记录填充输入缓冲区。
  • 若当前缓冲区的最小记录小于miniMax,那么就不可能将其放到归并段一的末尾,此时找到第二小且大于miniMax的记录放到归并段。
  • 当某一时刻输入缓冲区的所有记录都小于miniMax时,第一个初始归并段就构造结束。
  • 接着以同样的方式构造初始归并段二,依次类推直到待排文件为空。

最佳归并树

如果采用置换——选择排序构造初始归并段,并将每一个初始归并段看作一个叶子结点,归并段的长度作为结点的权值,则归并树的带权路径长度 W S L WSL WSL有以下公式成立:
W S L = 读磁盘的次数 = 写磁盘的次数 WSL=读磁盘的次数=写磁盘的次数 WSL=读磁盘的次数=写磁盘的次数

那么 W S L WSL WSL最小的树就是一棵哈夫曼树,从而可以通过构造一棵哈夫曼树以使存盘存取次数最小。在构造哈夫曼树树的过程中,如果初始归并段的数量无法构成严格的 k k k叉哈夫曼树,那么就需要补充长度为0的虚段,再进行构造。对于一棵 k k k叉归并树:

  • 如果 ( 初始归并段的数量 − 1 ) % ( k − 1 ) = 0 (初始归并段的数量-1)\%(k-1)=0 (初始归并段的数量1)%(k1)=0,则说明刚好可以构成严格 k k k叉哈夫曼树,此时不需要添加虚段。
  • 如果 ( 初始归并段的数量 − 1 ) % ( k − 1 ) = u ≠ 0 (初始归并段的数量-1)\%(k-1)=u\neq0 (初始归并段的数量1)%(k1)=u=0,则需要补充 ( k − 1 ) − u (k-1)-u (k1)u个虚段。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1144342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哨兵1和2号遥感数据请求失败

哨兵1和2号遥感数据请求失败 问题描述 在23年10月底下载哨兵数据的时候发现&#xff0c;22年12月前的哨兵数据都请求失败了&#xff0c;但是之后的数据都能够下载&#xff0c;是否是哨兵数据下载也有时间限制&#xff1f;网站上只能保存近一年来的数据呢&#xff1f; 解决方案…

C# | Chaikin算法 —— 计算折线对应的平滑曲线坐标点

Chaikin算法——计算折线对应的平滑曲线坐标点 本文将介绍一种计算折线对应的平滑曲线坐标点的算法。该算法使用Chaikin曲线平滑处理的方法&#xff0c;通过控制张力因子和迭代次数来调整曲线的平滑程度和精度。通过对原始点集合进行切割和插值操作&#xff0c;得到平滑的曲线坐…

基于SpringBoot+Vue的服装销售系统

基于SpringBootVue的服装销售平台的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 我的订单 登录界面 管理员界面 摘要 基于SpringBoot和Vue的服装销售系统…

【Hello Algorithm】滑动窗口内最大值最小值

滑动窗口介绍 滑动窗口是一种我们想象中的数据结构 它是用来解决算法问题的 我们可以想象出一个数组 然后再在这个数组的起始位置想象出两个指针 L 和 R 我们对于这两个指针做出以下规定 L 和 R指针只能往右移动L指针不能走到R指针的右边我们只能看到L指针和R指针中间的数字 …

【面向对象程序设计】Java大作业 汽车租赁管理系统V4.0

前言 自己大二时候使用JavaMysql写的租车系统大作业V4.0黑窗口版的一个记录&#xff0c;简简单单的黑窗口&#xff0c;不是炫酷的前后端分离也没用GUI&#xff0c;但功能完善&#xff0c;该有都有&#xff0c;当时得分也还是挺不错的 技术栈 Java (jdk8)Mysql 资源包内容 …

8、电路综合-基于简化实频的SRFT微带线的带通滤波器设计

8、电路综合-基于简化实频的SRFT微带线的带通滤波器设计 此处介绍微带线综合的巴特沃斯带通滤波器和切比雪夫带通滤波器的设计方法。对于理查德域的网络综合技术而言&#xff0c;这种带通综合和低通综合在本质上并无区别&#xff0c;因为理查德域函数是周期的。低通滤波器的SR…

一文讲明:企业知识库的作用和搭建方法

在现代商务环境中&#xff0c;企业面临着大量的信息和知识流动。这些信息和知识散落在各个部门、团队甚至个人之间&#xff0c;难以进行有效的整合和利用。而企业知识库的出现解决了这一问题。它提供了一个统一的平台&#xff0c;将分散的信息汇聚到一个集中的数据库中&#xf…

jenkins如何安装?

docker pull jenkins/jenkins:lts-centos7-jdk8 2.docker-compose.yml version: 3 services:jenkins:image: jenkins/jenkins:lts-centos7-jdk8container_name: my-jenkinsports:- "8080:8080" # 映射 Jenkins Web 界面端口volumes:- jenkins_home:/var/jenkins_h…

657. 机器人能否返回原点

657. 机器人能否返回原点 Java代码&#xff1a; class Solution {public boolean judgeCircle(String moves) {int[] x {0, 0, -1, 1};int[] y {1, -1, 0, 0};String str "UDLR";int xx 0, yy 0;for (int i 0; i < moves.length(); i) {xx x[str.indexOf(…

小美的修路(最小生成树练习)

本题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 题目&#xff1a; 样例&#xff1a; 输入 3 4 1 2 3 1 1 2 2 0 1 3 1 0 2 3 3 0 输出 2 1 3 思路&#xff1a; 由题意&#xff0c;这里建造的城市需要修路&#xff0c;且每个城市之间可以联通&#xff0c;且 是 1 …

网络协议--TCP的成块数据流

20.1 引言 在第15章我们看到TFTP使用了停止等待协议。数据发送方在发送下一个数据块之前需要等待接收对已发送数据的确认。本章我们将介绍TCP所使用的被称为滑动窗口协议的另一种形式的流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必…

安卓APP抓包环境配置

软件下载安装 所需软件 夜神模拟器 Fiddler platform-tools 下载地址 https://wwb.lanzoum.com/b01xhmk5e 密码:hv99 配置Fiddler 由于fiddler只默认抓取HTTP的请求&#xff0c;若想抓取HTTPS的请求&#xff0c;则需要设置HTTPS的各项值 设置HTTPS各项值 在fiddler菜单项…

思维模型 棘轮效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。由俭入奢易&#xff0c;由奢入俭难&#xff0c;到底因为啥&#xff1f;棘轮效应。 1 棘轮效应的应用 1.1 恋爱中的棘轮效应 小美是一个漂亮的女孩&#xff0c;家庭条件也不错&#xff0c;…

R2R 的一些小tip

批次间控制器(Run-to-run Controller)&#xff0c;以应对高混合生产的挑战。将最优配方参数与各种工业特征相关联的模型是根据历史数据离线训练的。预测的最优配方参数在线用于调整工艺条件。 批次控制(R2R control)是一种先进的工艺控制技术&#xff0c;可在运行(如批次或晶圆…

网络滤波器/网络滤波器/脉冲变压器要怎样进行测试,一般要测试哪些参数?

Hqst华强盛导读&#xff1a;网络滤波器/网络滤波器/脉冲变压器要怎样进行测试&#xff0c;一般要测试哪些参数&#xff1f;测试网络滤波器的测试方法和步骤如何&#xff0c;需用到哪些测试工具和仪器设备呢&#xff1f; 一&#xff0c;网络流量的监控和过滤能力测试&am…

教你自己动手搭建一个传奇游戏,自己和自己玩,找找当年的感觉

传奇游戏承载了一代人的青春记忆。在那个年代&#xff0c;很多人都会在网吧里玩传奇游戏&#xff0c;与朋友一起组队打怪、刷装备。这些经历不仅让很多8090终生难忘&#xff0c;也成为了我们青春岁月中最珍贵的回忆。 虽然现在的传奇游戏已经逐渐淡出了人们的视线&#xff0c;…

C++: 类和对象(上)

文章目录 1. 面向对象和面向对象初步认识2. 类的引入3. 类的访问限定符4. 类的定义类的两种定义方式成员变量名规则的建议 5. 类的作用域6. 类的实例化7. 类对象模型计算类对象的大小 类的实际存储方式 8. this指针this指针的引入this指针的特性 1. 面向对象和面向对象初步认识…

c++指针【1】

在C中&#xff0c;指针是一种特殊的变量&#xff0c;它存储了一个内存地址。C指针在处理内存、数组、函数参数传递、文件I/O、动态内存分配等方面有着重要的应用。 一个指针变量通常被声明为特定类型的指针。例如&#xff0c;一个整数类型的指针可以指向一个整数。在声明指针变…

【软考】13. 结构化开发方法

《系统分析与设计概述》 当前系统的物理模型 ——> 当前系统的逻辑模型 ——> 目标系统的逻辑模型 ——> 目标系统的物理模型系统开发的目的&#xff1a;当前系统的物理模型 ——> 目标系统的物理模型 系统设计基本原理 抽象、模块化&#xff08;逐步分解&#xf…