当前位置: 首页 > news >正文

数据结构——基本查找算法

目录

前言

一、 查找的概念

二、经典查找算法

1.顺序查找

(1)算法说明

(2)适用情况

(3)算法的优缺点:

(4)示例代码

2.分块查找

(1)算法说明

(2)举例说明

(3)性能分析与特点

(4)示例代码

3.二分查找

(1)算法说明

(2)算法的核心思路

(3)算法解释

(4)算法的优缺点

(5)示例代码


前言

        在程序的世界里,数据的价值在于被使用,而使用的第一步往往是“查找”。一个查找算法的选择,直接决定了程序的响应速度与资源消耗,尤其是在大数据时代,其影响更是天差地别。从O(n)到O(log n)的效率跃迁,背后是算法设计智慧的体现。

本文将聚焦于基本查找算法,深度剖析其内在原理与性能边界。我们将揭示:

  • 顺序查找为何是通用性的代表,却又在效率上存在天花板。

  • 二分查找如何通过“半区排除”策略,实现对数级的惊人效率,以及它必须遵守的“有序性”前提。

        通过对比它们的时空复杂度、适用场景与实现细节,我们旨在为你建立一个清晰的决策框架,让你在纷繁复杂的实际问题中,能迅速选出最合适的那把“钥匙”。

一、 查找的概念

        查找,又称搜索,是计算机科学中最基础、最核心的操作之一。它的定义非常直观:在一组数据集合中,找出满足特定条件的数据元素的过程。这个条件通常是数据元素的某个关键字(Key)与目标值相匹配。

        小到在手机通讯录中搜索一个名字,大到互联网搜索引擎在海量网页中检索相关信息,其底层都依赖于高效的查找算法。

评判一个查找算法的优劣,主要取决于两大核心因素,它们共同决定了查找操作的效率:

  • 数据本身的存储特性: 这是查找操作的“客观条件”。数据是有序还是无序?是存储在连续的内存空间(如数组)还是非连续的结构(如链表)?这些存储方式直接决定了我们可以采用哪种查找策略。

  • 查找算法自身的特点: 这是查找操作的“主观策略”。算法是采用逐个比较的“线性”思路,还是能够跳跃式搜索的“分治”策略?算法本身的逻辑直接影响了其时间复杂度和空间复杂度。


二、经典查找算法

1.顺序查找

(1)算法说明

        顺序查找,顾名思义,就是按照数据存储的自然顺序,从头到尾(或从尾到头)逐个进行比较,直到找到目标元素或遍历完整个数据集。

        它通常被称为“线性查找”,因为其查找路径在逻辑上是一条直线。这是一种最朴素、最直观的查找方法,甚至可以说是一种“不是算法的算法”——因为它直接体现了解决问题最原始的思维。正因为如此,它也是在所有其他更高效的算法都不适用时,那个“最没办法的办法”。

时间复杂度:O(n)

  • 在最坏情况下(目标元素在末尾或不存在),需要遍历整个包含 n 个元素的集合。

  • 在平均情况下,也需要遍历大约一半的元素。

  • 因此,顺序查找的时间复杂度为线性阶 O(n)。这意味着数据量增大一倍,最坏情况下的查找时间也大致会增加一倍。

(2)适用情况

  1. 数据存储无序
    这是顺序查找最主要的应用场景。当数据集完全没有经过排序,无法利用“有序性”来跳过某些比较时,只能进行逐个比对。

  2. 数据无法或不便于进行有效整理

    • 数据结构限制: 对于某些线性结构,如单向链表,我们无法像数组那样通过下标进行随机访问。即使链表中的数据是有序的,也无法实现像二分查找那样的跳跃式访问,只能进行顺序遍历。

    • 动态性极强: 如果数据集合更新极其频繁(频繁的插入和删除),维护其有序性的成本可能会高于顺序查找本身的成本。在这种情况下,直接进行顺序查找可能更实际。

    • 数据量很小: 当数据量非常小的时候(例如,只有几个或几十个元素),各种高效算法带来的性能提升微乎其微,而顺序查找实现简单,反而更具优势。

(3)算法的优缺点:

优点:

  • 算法简单,实现容易: 几行代码即可实现,不易出错。

  • 通用性强: 对数据的存储结构没有任何额外要求,既适用于数组,也适用于链表。

  • 无需预处理: 不需要数据事先有序,即来即用。

缺点:

效率低下: 

        当数据量 n 很大时,O(n) 的时间复杂度使其性能成为瓶颈,不适合高性能要求的应用。

(4)示例代码

#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>#include <strings.h>unsigned long long int count = 0;
#define DATA_NUM 1000*1000            // 内存元素个数/*** @brief  获取随机数据,并返回* @note   None* @param  range:控制获取的的随机数,至少是几位(个十百千万、十万、百万)* @retval None*/ 
int SEQ_SEARCH_Rand(int range)
{return rand()%((int)pow(10, rand()%6+range+1));
}/*** @brief  查找数据* @note   None* @param  data_p:指向要查找的数据的内存的指针*         len:    内存元素的个数*         num:   要查找的数据* @retval 成功:返回查找到的数据*         失败:返回小于0的数据*/ 
int SEQ_SEARCH_DataGet(unsigned int *data_p, unsigned int len, unsigned int num)
{// 1、判断传进来的内存是否为空或者有没有数据if ((data_p == NULL) || (len == 0))return -1;// 2、查找数据for (int i = 0; i < len; i++){// 计数执行了多少次count++;// 判断是否是找到的数据if (data_p[i] == num){printf("你要找的数据%d在第%d行!\n", data_p[i], i+1);return data_p[i];}}// 3、找不到数据,就返回-2return -2;
}/*** @brief  将数据存储到txt文件中(数据太大,所以放文本里面,充当显示)* @note   None* @param  data_p:要写入数据的内存*         file:  要读取数据的文件* @retval None*/ 
int SEQ_SEARCH_StoreData(unsigned int *data_p, const char *file)
{                 FILE *fp = fopen(file, "w");                    // 1、创建并打开文件(以写权限打开),并返回一个文件流控制符for (int i = 0; i < DATA_NUM; i++)              // 2、将海量数据写入到文本中{fprintf(fp, "%06d = %u\n", i, data_p[i]);}fclose(fp);                                     // 3、关闭文件
}// 主函数
int main(int argc, char const *argv[])
{// (1)、产生无序数据,并赋值给相应的内存空间// 1、使用时间作为随机数的变化量srand(time(NULL));// 2、申请堆空间,用来存放数据 --- PS:栈空间=8M, 堆空间:几个G(看你的电脑的硬盘和系统支持度)unsigned int *data_p = malloc(sizeof(unsigned int)*DATA_NUM);bzero(data_p, sizeof(unsigned int)*DATA_NUM);// 3、存放数据for (unsigned int i = 0; i < DATA_NUM ; i++){*(data_p+i) = SEQ_SEARCH_Rand(1);}// (2)、将数据加载到.txt文件中SEQ_SEARCH_StoreData(data_p, "./顺序查找文本.txt"); // 这里只做显示使用(通常来讲,是读取文件数据到堆空间,然后再处理数据的)// (3)、查找数据int check_num = 0;while (1){// 1、输入要查找的数据printf("请输入你要找的数据(正整数):\n");scanf("%d", &check_num);// 2、返回找的数据int ret = SEQ_SEARCH_DataGet(data_p, DATA_NUM, check_num);if (ret == -1){printf("data_p指向的内存的数据为空的\n");}else if(ret == -2){printf("没有在data_p指向的内存中找到数据!\n");}printf("一共找了%llu次!\n", count);count = 0;}return 0;
}

2.分块查找

(1)算法说明

        分块查找,又称索引顺序查找,它实质上是一种通过建立索引来优化查找过程的算法。它将查找过程清晰地分为两个步骤:

  1. 在索引中确定块:

    • 将整个数据集分成若干个子集,称为“块”。

    • 为每一块建立一个索引项,索引项通常包含该块中数据的最大关键字(或最小关键字)以及该块的起始地址。

    • 查找时,首先在索引表中进行查找,确定目标数据可能存在于哪一块中。因为索引表是有序的,所以这一步可以使用高效的查找算法(如二分查找)。

  2. 在确定的块内进行查找:

    • 根据索引项提供的地址,找到对应的数据块。

    • 然后在该数据块内进行查找。由于块内的数据可以是无序的,因此这一步通常采用顺序查找

(2)举例说明

假设有一个数组 [22, 12, 9, 33, 42, 48, 60, 55, 70, 65, 88, 80],我们将其分为3块。

  • 块1: [22, 12, 9, 33] -> 最大关键字:33

  • 块2: [42, 48, 60, 55] -> 最大关键字:60

  • 块3: [70, 65, 88, 80] -> 最大关键字:88

建立索引表: 索引表是一个有序表,记录了每块的最大关键字和起始地址

最大关键字起始地址
330
604
888

查找:

  1. 索引查找: 在索引表中查找 48。因为 33 < 48 <= 60,所以确定 48 在第二块。

  2. 块内查找: 根据索引表,找到第二块的起始地址是 4,然后在该块 [42, 48, 60, 55] 内进行顺序查找,成功找到 48。

(3)性能分析与特点

时间复杂度:

  • 分块查找的性能介于顺序查找和二分查找之间。假设有 n 个数据,平均分成 b 块,每块有 s 个元素(n = b * s)。

    • 索引查找的平均时间复杂度为 O(log b) 或 O(b)(取决于索引查找算法)。

    • 块内查找的平均时间复杂度为 O(s)。

    • 总的时间复杂度约为 O(log b + s)。当块数 b 和块内元素数 s 接近 √n 时,性能较优。

适用条件:

  • 数据集合庞大,且难以整体排序,但可以分块。

  • 块内数据可以无序,但块与块之间必须有序(即后一块的所有关键字必须大于前一块的最大关键字)。

  • 非常适合数据库索引文件系统等外部存储查找场景。

(4)示例代码

#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <stdbool.h>unsigned long long int count = 0;
#define STRING_MAX_LEN 20       // 字符串的最大长度
#define DATA_NUM 1000*1000      // 内存元素个数/*** @brief  生成随机字符数组(1)* @note   None* @param  None* @retval 成功:返回随机字符数组*         失败:NULL*/ 
char* BLOCK_SEARCH_Rand(void)
{int len = rand()%(STRING_MAX_LEN);  // 规定字符串的最长的长度为STRING_MAX_LENlen = ((len<2)?2:len);              // 规定字符串的最短的长度为2char* s =calloc(1, len);            // 申请len个字节的堆空间if (s==NULL)return NULL;char letter[] = {'a', 'A'};         // 随机你的字符为小写a还是大写Afor (int i = 0; i < len; i++)    s[i] = letter[rand()%2] + rand()%26;// 随机你的字符为小写a还是大写A,再在此基础上// 加0到25的数据,从而形成a到z,A到Z的随机字母// 返回生成的随机字符数组return s;   
}/*** @brief  将数据存储到txt文件中* @note   None* @param  data_p:二维数组(里面的每一个一维数组,都是字符数组)*         file:  要读取数据的文件* @retval None*/ 
int  BLOCK_SEARCH_StoreData(char data_p[][STRING_MAX_LEN], const char *file)
{                 FILE *fp = fopen(file, "w");                    // 1、创建并打开文件(以写权限打开),并返回一个文件流控制符for (int i = 0; i < DATA_NUM; i++)              // 2、将海量数据写入到文本中{fprintf(fp, "%06d = %s\n", i, data_p[i]);   }fclose(fp);                                     // 3、关闭文件
}/*** @brief  在索引(目录)中找到数据所属分块* @note   根据字符串开头字符的大小写来寻找* @param  data_p:二维数组(里面的每一个一维数组,都是字符数组)*         index: 二级指针(里面的每一个一级指针,都指向了一个字符数组)* @retval None*/ 
void BLOCK_SEARCH_CreateIndex(char data_p[][STRING_MAX_LEN], int **index )
{// 1、统计各个首字符出现的频次int n[52] = {0};                        // ['a'...'z'和'A'...'Z'共:52个]for (int i = 0; i < DATA_NUM; i++)      // 在所有数据中找每一行字符串的首字符{                                 // b                      Eint pos = (data_p[i][0]>='a')?(data_p[i][0]-'a'):(data_p[i][0]-'A'+26); /*说明:找到的首字符的ASCII值,其值大于'a',则为小写(算出其位置)其值小于'a',则为大写(算出其位置)*/ n[pos]++;                           // 统计各个首字符出现的频次}// 2、给index分配内存for (int i = 0; i < 52; i++){index[i] = calloc(1+n[i], sizeof(int));                                  }// 3、记录每个字符出现的行号for (int i = 0; i < DATA_NUM; i++){int pos = (data_p[i][0]>='a')?(data_p[i][0]-'a'):(data_p[i][0]-'A'+26); // 记录输入的字符串的首个字符是52个大小写字母数组中的第几个位置int j = ++index[pos][0];                                                // 在分块好的块中排第几个index[pos][j] = i;                                                      // 总行数赋值给对应字符串的行数}  }// 主函数
int main(int argc, char const *argv[])
{// (1)、产生无序数据(字符串),并赋值给相应的内存空间// 1、使用时间作为随机数的变化量srand(time(NULL));// 2、申请堆空间,用来存放数据 --- PS:栈空间=8M, 堆空间:几个G(看你的电脑的硬盘和系统支持度)char (*data_p)[STRING_MAX_LEN] = calloc(DATA_NUM, STRING_MAX_LEN);// 3、存放数据for (unsigned int i = 0; i < DATA_NUM ; i++){char *s = BLOCK_SEARCH_Rand();strncpy(data_p[i], s, strlen(s));free(s);}// (2)、再将数据加载到.txt文件中BLOCK_SEARCH_StoreData(data_p, "./分块查找文本.txt");// (3)、利用索引,进行查找char str[32] = {0};printf("请输入你要查找的字符串:\n");int **index = calloc(52, sizeof(int*));BLOCK_SEARCH_CreateIndex(data_p, index);while(1){// 从键盘接收一个待查的字符串,并去掉回车符bzero(str, 32);fgets(str, 32, stdin);strtok(str, "\n");bool done = false;for (int i = 1; i < DATA_NUM; i++){count++;// 小写字母[00到25],大写字母[26到51]int pos = (str[0]>='a')?(str[0]-'a'):(str[0]-'A'+26); if ( (i<=index[pos][0]) && (strcmp(data_p[index[pos][i]], str)==0) ) {printf("你要找的字符串在第%d行!\n", index[pos][i]);done = true;break;}else if(i>index[pos][0])break;}if(!done)printf("没有你要找的字符串!\n");printf("一共找了%llu次!\n", count);count = 0;}return 0;
}

3.二分查找

(1)算法说明

        二分查找算法是计算机科学中最优美且高效的算法之一。它有一个重要的前提条件待查找的数据集必须是有序的(无论是升序还是降序)。

        这个条件意味着,如果数据本身是无序的,我们需要在查找前对其进行一次排序。虽然排序本身有成本,但如果数据量较大且在一段时间内相对稳定,不会发生大面积更新,那么一次排序的开销可以被后续大量高效的查找操作所分摊,从而带来整体性能的显著提升。在这种情况下,使用二分查找是极佳的选择。

时间复杂度:

二分查找每次比较都能将搜索范围缩小一半,这是一种指数级的缩减速度。因此,即使面对海量数据,它也能在极少的步骤内完成查找,其时间复杂度为对数阶 O(log n),效率远高于顺序查找的 O(n)。


(2)算法的核心思路

        二分查找的核心思想是 “分而治之” 。它通过不断将有序数据集对半分割,并通过中间元素与目标值的比较来排除一半的搜索空间,从而快速定位目标。

具体步骤(以升序数组为例):

  1. 初始化边界: 设定两个指针,low 指向搜索区间的起始位置,high 指向搜索区间的末尾位置。

  2. 循环条件: 只要 low 指针没有超过 high 指针(即 low <= high),就继续执行步骤3-5。

  3. 计算中间点: 取当前搜索区间 [low, high] 的中间位置 mid。通常使用 mid = low + (high - low) / 2 来计算,这样可以有效防止整数溢出。

  4. 比较与判断:

    • 如果 array[mid] 等于目标值,查找成功,返回 mid

    • 如果 array[mid] 小于目标值,说明目标值只可能存在于右半部分。调整 low 指针为 mid + 1,在右半区间继续查找。

    • 如果 array[mid] 大于目标值,说明目标值只可能存在于左半部分。调整 high 指针为 mid - 1,在左半区间继续查找。

  5. 重复: 回到步骤2,在新的搜索区间内继续执行。

  6. 查找失败: 如果循环结束仍未找到目标值,则查找失败。

简单来说: “从中间开始找,比较一次就能排除一半的数据。再在剩下的一半里重复这个过程,直到找到目标或确定目标不存在。”


(3)算法解释

假设我们有如图所示的数组:

现在想要在此有序数组(从小到大)的数据中,查找45:

  • left 指针指向索引 0(元素 4)。

  • right 指针指向索引 7(元素 56)。

  • middle 指针通过计算得出,指向索引 3(元素 22)。计算方式为:middle = (left + right) / 2 = (0 + 7) / 2 = 3(向下取整)。

第一步:比较与决策

  • 将中间元素 array[middle](即 22)与目标值 45 进行比较。

  • 比较结果: 22 < 45

  • 逻辑判断: 由于数组是升序的,且中间值小于目标值,这意味着目标值 45 只可能存在于当前中间点的右侧区域

  • 操作: 将 left 指针移动到 middle + 1 的位置,即索引 4(元素 23)。这样,搜索范围被缩小为右半部分:[23, 23, 45, 56](索引 4 到 7)。


第二步:在新的范围内继续查找

  • 新的搜索区间:left = 4right = 7

  • 计算新的 middle 索引:(4 + 7) / 2 = 5(向下取整)。

  • 此时 middle 指向索引 5,对应的元素是 23

  • 比较结果: 23 < 45

  • 逻辑判断: 目标值仍然在右侧。

  • 操作: 再次将 left 指针移动到 middle + 1 的位置,即索引 6(元素 45)。搜索范围进一步缩小为 [45, 56](索引 6 到 7)。


第三步:找到目标

  • 新的搜索区间:left = 6right = 7

  • 计算新的 middle 索引:(6 + 7) / 2 = 6(向下取整)。

  • 此时 middle 指向索引 6,对应的元素是 45

  • 比较结果: 45 == 45

  • 结论: 查找成功!目标值 45 在数组中的索引位置为 6

步骤leftrightmiddlearray[middle]比较结果操作
初始0732222 < 45搜索右半部分,left = 4
第1次4752323 < 45搜索右半部分,left = 6
第2次6764545 == 45查找成功,返回索引 6

(4)算法的优缺点

优点:

  • 效率极高: O(log n) 的时间复杂度使其在处理大规模有序数据时优势巨大。

缺点:

  • 依赖有序数据: 前提条件苛刻,必须保证数据集有序。

  • 仅适用于顺序存储结构: 依赖下标随机访问,因此适用于数组,但不适用于链表等非随机访问的数据结构。

稳定性:

 二分查找本身是查找算法,不涉及数据移动,故无稳定性问题。


(5)示例代码

#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <strings.h>unsigned long long int count = 0;
#define DATA_NUM 1000*1000            // 内存元素个数/*** @brief  获取随机数据,并返回* @note   None* @param  range:控制获取的的随机数,至少是几位(个十百千万、十万、百万)* @retval None*/ 
int BIN_SEARCH_Rand(int range)
{return rand()%((int)pow(10, rand()%6+range+1));
}/*** @brief  查找数据* @note   二分查找法* @param  data_p:指向要查找的数据的内存的指针*         len:    内存元素的个数*         num:   要查找的数据* @retval 成功:返回查找到的数据*         失败:返回小于0的数据*/ 
int BIN_SEARCH_DataGet(unsigned int *data_p, unsigned int len, unsigned int num)
{// 1、判断传进来的内存是否为空或者有没有数据if ((data_p == NULL) || (len == 0))return -1;// 2、查找数据int left     = 0;int right    = len-1;int middle   = 0;while (left<=right)                 // 二分法的极限,已经将所有的数据比较完{count++;                        // 记录查询了几次            middle = (left+right)/2;        // 获取中间的位置if (num == data_p[middle])      // 判断是否找到了数据,并返回该值的行数和值{printf("你要找的数据在第%d行", middle);return data_p[middle];}if (num < data_p[middle])       // num小于中间值,则其右边的所有数据包括原先的中间值都可以不用计算right = middle-1;else                            // num大于中间值,则其左边的所有数据包括原先的中间值都可以不用计算left  = middle+1;}// 3、找不到数据return -2;
}/*** @brief  将数据存储到txt文件中(数据太大,所以放文本里面,充当显示)* @note   None* @param  data_p:要写入数据的内存*         file:  要读取数据的文件* @retval None*/ 
int BIN_SEARCH_StoreData(unsigned int *data_p, const char *file)
{                 FILE *fp = fopen(file, "w");                    // 1、创建并打开文件(以写权限打开),并返回一个文件流控制符for (int i = 0; i < DATA_NUM; i++)              // 2、将海量数据写入到文本中{fprintf(fp, "%06d = %u\n", i, data_p[i]);}fclose(fp);                                     // 3、关闭文件
}/*** @brief  交换两个数据* @note   None* @param  num1:指向要交换的整型数据1的指针*         num2:指向要交换的整型数据2的指针* @retval None*/ 
void __Swap(unsigned int *num1, unsigned int *num2)
{unsigned int tmp = 0;    // 交换两个数据的中间值tmp   = *num1;           // 交换两个数据的值  *num1 = *num2;*num2 = tmp;
}/*** @brief  快速排序* @note   None* @param  data_p: 指向要排序数据内存的指针*         len:     数据内存的长度* @retval 成功:返回0*         失败:返回-1*/ 
int QUICK_SORT_DataSort(unsigned int *data_p, unsigned int len)
{// 10// 1、退出条件if (len <= 1)return 0;// 2、int i = 0;                         // 0int j = len-1;  // 不断递进的过程   // 9while (i<j){// 从右到左比较,顺序j--, 逆序交换while ( (data_p[i] <= data_p[j]) && (i<j)){j--;}__Swap(&data_p[i], &data_p[j]);// 从左到右比较,顺序i++,逆序交换while ( (data_p[i] <= data_p[j]) && (i<j)){i++;}__Swap(&data_p[i], &data_p[j]);}// 3、不断逼近结果QUICK_SORT_DataSort(data_p, i);             // 解决左边序QUICK_SORT_DataSort(data_p+i+1, len-i-1);   // 解决右边序
}// 主函数
int main(int argc, char const *argv[])
{// (1)、产生无序数据,并赋值给相应的内存空间// 1、使用时间作为随机数的变化量srand(time(NULL));// 2、申请堆空间,用来存放数据 --- PS:栈空间=8M, 堆空间:几个G(看你的电脑的硬盘和系统支持度)unsigned int *data_p = malloc(sizeof(unsigned int)*DATA_NUM);bzero(data_p, sizeof(unsigned int)*DATA_NUM);// 3、存放数据for (unsigned int i = 0; i < DATA_NUM ; i++){*(data_p+i) = BIN_SEARCH_Rand(1);}// (2)、先排序,再将数据加载到.txt文件中QUICK_SORT_DataSort(data_p, DATA_NUM);BIN_SEARCH_StoreData(data_p, "./二分查找文本.txt"); // 这里只做显示使用(通常来讲,是读取文件数据到堆空间,然后再处理数据的)// (3)、查找数据int check_num = 0;while (1){// 1、输入要查找的数据printf("请输入你要找的数据(正整数):\n");scanf("%d", &check_num);// 2、返回找的数据int ret = BIN_SEARCH_DataGet(data_p, DATA_NUM, check_num);if (ret == -1){printf("data_p指向的内存的数据为空的\n");}else if(ret == -2){printf("没有在data_p指向的内存中找到数据!\n");}printf("一共找了%llu次!\n", count);count = 0;}return ;
}

http://www.dtcms.com/a/412176.html

相关文章:

  • 互联网视频云平台EasyDSS赋能在线教育创新与变革
  • 邹城住房城乡建设部网站网页设计模板html代码个人介绍
  • 35 岁后肌肤暗沉与细纹改善:BKY(莱德因)美国进口 EGT+AKG+SOD 王炸御龄组合的作用机制与应用价值​
  • 长沙做网站开发价格群晖 wordpress 中文
  • Ubuntu20.04复现ReKep论文详细指南,记录自用
  • 金鹏建设集团网站团员建设网站
  • 兰州新区建设局网站免费网络推广网址
  • h5网站系统wordpress导入数据库
  • postman 自动设置token
  • 杭州建平台网站公司网站建设教程asp
  • 【读书笔记】架构整洁之道 P5-2 软件架构
  • 网站搭建十大品牌公司1688一键铺货到拼多多
  • ai做网站ppt万能模板免费下载
  • 不同地区(语言)windows系统的字符串乱码问题
  • 英文外贸商城网站设计wordpress网站怎么加速
  • 软考-系统架构设计师 关系数据库详细讲解
  • 国外网站购物西安到北京火车票查询
  • 视频广告网站h5 技术做健康类网站
  • Java基础-面向对象复习知识3
  • [Windows] VarCalc v0.0.1
  • 网站开发培训北京有用建站宝盒做网站的吗
  • 亚马逊网站青岛鑫隆建设集团网站
  • 南宁最高端网站建设局域网内部网站建设app下载
  • 电机东莞网站建设建立网站时首先考虑的问题
  • 有没有什么网站做泰国的东西免费国内linux服务器
  • 销售网站制作电话甘肃交通建设监理公司网站
  • sqlite是什么
  • React学习第二天——表单控件
  • wordpress多站点详细设置(图解)杭州网络科技公司排名
  • 网站建设公司ejiew网站建设的公司哪家好