从C语言看数据结构和算法:复杂度决定性能

微软技术分享 微软全球最有价值专家
全栈领域优质创作者
博客专家认证
2025-02-04 20:33:54

在计算机科学中,算法是解决问题的核心工具。当我们设计或选择一个算法时,通常需要考虑两个关键因素:时间复杂度和空间复杂度。这两个指标帮助我们衡量算法的效率和资源消耗情况。本文将深入探讨C语言中常见的数据结构及其相关算法的复杂度分析,并通过代码示例进行具体说明。那现在,一起来看看吧!!!

https://i-blog.csdnimg.cn/direct/82bf9044e2db4d419604de2d118da9bb.jpeg

那接下来就让我们开始遨游在知识的海洋!

正文

一 时间复杂度

  • 定义:时间复杂度是衡量算法执行时间长短的一个标准,它反映了算法随着输入规模增大而增长的趋势。通常用“大O符号”(O-notation)来表示。

1. 常数时间复杂度 O(1)

  • 常数时间复杂度意味着无论输入规模如何变化,算法的执行时间都保持不变。例如,访问数组中的某个元素就是O(1)操作。

例:

 
#include <stdio.h>

int getElement(int arr[], int index, int size) {
    if (index >= 0 && index < size) {
        return arr[index]; // 直接访问数组元素,时间复杂度为O(1)
    } else {
        return -1; // 错误处理
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    printf("%d
", getElement(arr, 2, 5)); // 输出3
    return 0;
}

2. 线性时间复杂度 O(n)

  • 线性时间复杂度表示算法的执行时间与输入规模成正比。例如,遍历一个数组的所有元素就是O(n)操作。

例·:

 
#include <stdio.h>

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]); // 遍历数组,时间复杂度为O(n)
    }
    printf("
");
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    printArray(arr, 5); // 输出整个数组
    return 0;
}

3. 对数时间复杂度 O(log n)

  • 对数时间复杂度常见于二分查找等基于分治策略的算法中。其特点是指数级减少的搜索范围

例·:

 
#include <stdio.h>

int binarySearch(int arr[], int target, int left, int right) {
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid; // 找到目标,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1; // 在右半部分继续搜索
        } else {
            right = mid - 1; // 在左半部分继续搜索
        }
    }
    return -1; // 未找到目标
}

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int target = 7;
    int result = binarySearch(arr, target, 0, 9);
    if (result != -1) {
        printf("Found %d at index %d
", target, result);
    } else {
        printf("%d not found in array
", target);
    }
    return 0;
}

4. 平方时间复杂度 O(n^2)

  • 平方时间复杂度通常出现在嵌套循环结构中,如冒泡排序和选择排序。

冒泡排序示例:

#include <stdio.h>

void bubbleSort(int arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("
");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int size = sizeof(arr)/sizeof(arr[0]);
    bubbleSort(arr, size);
    printf("Sorted array: 
");
    printArray(arr, size);
    return 0;
}

5. 指数时间复杂度 O(2^n)

  • 指数时间复杂度通常出现在递归调用未进行优化且问题规模迅速增大的情况下,如斐波那契数列的直接递归实现。
 
// 未经优化的斐波那契数列计算
#include <stdio.h>

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

int main() {
    int n = 10;
    printf("Fibonacci number is %d
", fibonacci(n));
    return 0;
}

二 空间复杂度

(1)空间复杂度的定义与重要性

  • 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。它反映了算法在执行过程中对内存使用的需求,是评价算法效率的重要指标之一。与时间复杂度类似,空间复杂度也使用大O记号来渐进地表示

(2)常见的空间复杂度类型及介绍

1.常数空间复杂度 O(1)
  • 算法所需的额外空间是一个常数,与输入规模无关。这意味着不随着输入数据的增加而增加额外的存储空间。例如,在交换变量的操作中,只使用了一个额外的变量temp,无论输入的数据是什么规模,都只需要常数级别的额外空间。

例如,一个简单的交换两个整数值的函数:

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);
    printf("x = %d, y = %d
", x, y); // 输出: x = 10, y = 5
    return 0;
}

在这个例子中,swap 函数只使用了一个额外的变量 temp 来存储临时值,因此它的空间复杂度是 O(1)。

2.线性空间复杂度 O(n)
  • 算法所需的额外空间与输入规模成线性关系。例如,需要一个与输入规模相等的数组来存储数据,或者递归函数中每次调用都会占用一定的栈空间,调用次数与输入参数n成线性关系。常见的具有线性空间复杂度的算法包括复制数组字符串拼接等。

例如,一个计算数组中所有元素之和的函数:

#include <stdio.h>

int sumArray(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    printf("Sum = %d
", sumArray(arr, size)); // 输出: Sum = 15
    return 0;
}

这里,除了几个固定大小的变量(如 sum 和循环计数器 i)外,没有使用额外的内存。但是,由于我们假设数组本身占用的空间也计入空间复杂度(尽管它是作为输入提供的),所以整个程序的空间复杂度可以视为 O(n)(其中 n 是数组的大小)。然而,仅就 sumArray 函数而言,其额外使用的空间仍然是 O(1)。

3.多项式空间复杂度 O(n^k)(k为常数)
  • 算法所需的额外空间与输入规模的幂次关系成正比。例如,某些嵌套循环算法可能需要二维数组,导致空间复杂度为O(n^2)。这类算法的空间需求随着输入规模的增加而迅速增长

例如,打印一个 n×n 矩阵的元素:

#include <stdio.h>

void printMatrix(int matrix[][10], int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("
");
    }
}

int main() {
    int matrix[10][10] = { /* 初始化矩阵 */ };
    // 假设矩阵已经以某种方式被填充
    printMatrix(matrix, 10); // 这里 n 被硬编码为 10,但一般情况下可以是任何正整数
    return 0;
}

注意:

  • 在这个例子中,我为了简化而假设矩阵的第二维大小为常量 10(这是不常见的做法,通常我们会让两个维度都是可变的)。在实际应用中,更常见的做法是声明一个完全可变的二维数组或使用动态内存分配。即使这样,如果我们考虑矩阵本身所占用的空间,那么空间复杂度仍然是 O(n^2)
4. 动态分配的内存(可变空间复杂度)

当使用 malloccallocrealloc 等函数动态分配内存时,空间复杂度可能变得不那么直观。它取决于运行时决定分配的内存量。例如:

#include <stdio.h>
#include <stdlib.h>

void createAndFillArray(int **array, int size) {
    *array = (int *)malloc(size * sizeof(int));
    if (*array == NULL) {
        fprintf(stderr, "Memory allocation failed
");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < size; i++) {
        (*array)[i] = i * i; // 例如,用平方值填充数组
    }
}

int main() {
    int size = 100; // 可以根据需要改变大小
    int *array = NULL;
    createAndFillArray(&array, size);
    // 使用数组...
    free(array); // 不要忘记释放分配的内存!
    return 0;
}

在这个例子中,createAndFillArray 函数根据传入的 size 参数动态分配一个整数数组。因此,该函数的空间复杂度是 O(size),即 O(n)(如果我们将 size 视为输入 n参数)。注意,这里的空间复杂度是指除了输入参数和固定大小的变量之外额外分配的内存量。

(3)空间复杂度的分析方法

分析算法的空间复杂度时,需要考虑以下几个方面:

  • 算法本身所占用的存储空间:这取决于算法书写的长短和所使用的数据结构。
  • 输入输出数据所占用的存储空间:这是由要解决的问题决定的,不随算法的不同而改变。
  • 算法在运行过程中临时占用的存储空间:这是随算法的不同而异的,也是空间复杂度分析的重点。需要关注算法在执行过程中是否需要额外的辅助空间,以及这些空间的增长情况。

综上所述:

  • 空间复杂度是衡量算法内存效率的重要指标之一。通过了解不同类型的空间复杂度及其特点和分析方法,可以选择最合适的算法来解决特定问题,从而提高系统性能。

三 空间复杂度与时间复杂度的关系

  • 时间复杂度和空间复杂度是算法的两个主要性能指标,它们之间存在一定的权衡关系。有时为了追求较好的时间复杂度,可能会牺牲一定的空间复杂度;反之亦然。因此,在设计算法时需要根据具体问题的需求和资源限制进行综合考虑

四 常见算法的复杂度分析

1. 线性搜索

  • 线性搜索是一种简单的查找算法,它逐个检查列表中的元素,直到找到目标值或遍历完整个列表。

代码展示:

 
#include <stdio.h>

int linearSearch(int arr[], int size, int target) {
    for (int i = 0; i < size; i++) {
        if (arr[i] == target) {
            return i; // 找到目标值,返回索引
        }
    }
    return -1; // 未找到目标值,返回-1
}

int main() {
    int arr[] = {2, 4, 6, 8, 10};
    int target = 6;
    int result = linearSearch(arr, 5, target);
    if (result != -1) {
        printf("Element found at index %d
", result);
    } else {
        printf("Element not found in the array
");
    }
    return 0;
}
  • 时间复杂度:O(n) 在最坏情况下,需要比较n次才能找到目标值或确定其不存在。因此,线性搜索的时间复杂度为O(n)。
  • 空间复杂度:O(1) 除了输入数组和几个辅助变量外,不需要额外的存储空间。因此,空间复杂度为O(1)。

2. 二分搜索

  • 二分搜索要求输入数组是有序的,它通过不断将搜索范围减半来快速定位目标值。

代码展示:

 
#include <stdio.h>

int binarySearch(int arr[], int size, int target) {
    int left = 0, right = size - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid; // 找到目标值,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1; // 在右半部分继续搜索
        } else {
            right = mid - 1; // 在左半部分继续搜索
        }
    }
    return -1; // 未找到目标值,返回-1
}

int main() {
    int arr[] = {2, 4, 6, 8, 10};
    int target = 6;
    int result = binarySearch(arr, 5, target);
    if (result != -1) {
        printf("Element found at index %d
", result);
    } else {
        printf("Element not found in the array
");
    }
    return 0;
}
  • 时间复杂度:O(log n) 每次迭代都将搜索范围减半,因此二分搜索的时间复杂度为O(log n),其中n是数组的大小。
  • 空间复杂度:O(1) 同样地,除了输入数组和几个辅助变量外,不需要额外的存储空间。因此,空间复杂度为O(1)。

3. 冒泡排序

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

#include <stdio.h>

void bubbleSort(int arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换arr[j]和arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("
");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int size = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, size);
    printf("Sorted array: 
");
    printArray(arr, size);
    return 0;
}
  • 时间复杂度:O(n^2) 在最坏情况下,需要进行n*(n-1)/2次比较和可能的交换操作,因此冒泡排序的时间复杂度为O(n^2)。
  • 空间复杂度:O(1) 冒泡排序是原地排序算法,不需要额外的存储空间。因此,空间复杂度为O(1)。

快乐的时光总是短暂,咱们下篇博文再见啦!!!不要忘了,给小编点点赞和收藏支持一下,在此非常感谢!!!


文章来源: https://blog.csdn.net/z15879084549/article/details/145301325
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


...全文
35 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表的源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针的指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450

3,166

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 微软技术分享
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧