练习34:动态数组

原文:Exercise 34: Dynamic Array

译者:飞龙

动态数组是自增长的数组,它与链表有很多相同的特性。它通常占据更少的空间,跑得更快,还有一些其它的优势属性。这个练习会涉及到它的一些缺点,比如从开头移除元素会很慢,并给出解决方案(只从末尾移除)。

动态数组简单地实现为void **指针的数组,它是预分配内存的,并且指向数据。在链表中你创建了完整的结构体来储存void *value指针,但是动态数组中你只需要一个储存它们的单个数组。也就是说,你并不需要创建任何其它的指针储存上一个或下一个元素。它们可以直接索引。

我会给你头文件作为起始,你需要为实现打下它们:

  1. #ifndef _DArray_h
  2. #define _DArray_h
  3. #include <stdlib.h>
  4. #include <assert.h>
  5. #include <lcthw/dbg.h>
  6. typedef struct DArray {
  7. int end;
  8. int max;
  9. size_t element_size;
  10. size_t expand_rate;
  11. void **contents;
  12. } DArray;
  13. DArray *DArray_create(size_t element_size, size_t initial_max);
  14. void DArray_destroy(DArray *array);
  15. void DArray_clear(DArray *array);
  16. int DArray_expand(DArray *array);
  17. int DArray_contract(DArray *array);
  18. int DArray_push(DArray *array, void *el);
  19. void *DArray_pop(DArray *array);
  20. void DArray_clear_destroy(DArray *array);
  21. #define DArray_last(A) ((A)->contents[(A)->end - 1])
  22. #define DArray_first(A) ((A)->contents[0])
  23. #define DArray_end(A) ((A)->end)
  24. #define DArray_count(A) DArray_end(A)
  25. #define DArray_max(A) ((A)->max)
  26. #define DEFAULT_EXPAND_RATE 300
  27. static inline void DArray_set(DArray *array, int i, void *el)
  28. {
  29. check(i < array->max, "darray attempt to set past max");
  30. if(i > array->end) array->end = i;
  31. array->contents[i] = el;
  32. error:
  33. return;
  34. }
  35. static inline void *DArray_get(DArray *array, int i)
  36. {
  37. check(i < array->max, "darray attempt to get past max");
  38. return array->contents[i];
  39. error:
  40. return NULL;
  41. }
  42. static inline void *DArray_remove(DArray *array, int i)
  43. {
  44. void *el = array->contents[i];
  45. array->contents[i] = NULL;
  46. return el;
  47. }
  48. static inline void *DArray_new(DArray *array)
  49. {
  50. check(array->element_size > 0, "Can't use DArray_new on 0 size darrays.");
  51. return calloc(1, array->element_size);
  52. error:
  53. return NULL;
  54. }
  55. #define DArray_free(E) free((E))
  56. #endif

这个头文件向你展示了static inline的新技巧,它就类似#define宏的工作方式,但是它们更清楚,并且易于编写。如果你需要创建一块代码作为宏,并且不需要代码生成,可以使用static inline函数。

为链表生成for循环的LIST_FOREACH不可能写为static inline函数,因为它需要生成循环的内部代码块。实现它的唯一方式是灰调函数,但是这不够块,并且难以使用。

之后我会修改代码,并且让你创建DArray的单元测试。

  1. #include "minunit.h"
  2. #include <lcthw/darray.h>
  3. static DArray *array = NULL;
  4. static int *val1 = NULL;
  5. static int *val2 = NULL;
  6. char *test_create()
  7. {
  8. array = DArray_create(sizeof(int), 100);
  9. mu_assert(array != NULL, "DArray_create failed.");
  10. mu_assert(array->contents != NULL, "contents are wrong in darray");
  11. mu_assert(array->end == 0, "end isn't at the right spot");
  12. mu_assert(array->element_size == sizeof(int), "element size is wrong.");
  13. mu_assert(array->max == 100, "wrong max length on initial size");
  14. return NULL;
  15. }
  16. char *test_destroy()
  17. {
  18. DArray_destroy(array);
  19. return NULL;
  20. }
  21. char *test_new()
  22. {
  23. val1 = DArray_new(array);
  24. mu_assert(val1 != NULL, "failed to make a new element");
  25. val2 = DArray_new(array);
  26. mu_assert(val2 != NULL, "failed to make a new element");
  27. return NULL;
  28. }
  29. char *test_set()
  30. {
  31. DArray_set(array, 0, val1);
  32. DArray_set(array, 1, val2);
  33. return NULL;
  34. }
  35. char *test_get()
  36. {
  37. mu_assert(DArray_get(array, 0) == val1, "Wrong first value.");
  38. mu_assert(DArray_get(array, 1) == val2, "Wrong second value.");
  39. return NULL;
  40. }
  41. char *test_remove()
  42. {
  43. int *val_check = DArray_remove(array, 0);
  44. mu_assert(val_check != NULL, "Should not get NULL.");
  45. mu_assert(*val_check == *val1, "Should get the first value.");
  46. mu_assert(DArray_get(array, 0) == NULL, "Should be gone.");
  47. DArray_free(val_check);
  48. val_check = DArray_remove(array, 1);
  49. mu_assert(val_check != NULL, "Should not get NULL.");
  50. mu_assert(*val_check == *val2, "Should get the first value.");
  51. mu_assert(DArray_get(array, 1) == NULL, "Should be gone.");
  52. DArray_free(val_check);
  53. return NULL;
  54. }
  55. char *test_expand_contract()
  56. {
  57. int old_max = array->max;
  58. DArray_expand(array);
  59. mu_assert((unsigned int)array->max == old_max + array->expand_rate, "Wrong size after expand.");
  60. DArray_contract(array);
  61. mu_assert((unsigned int)array->max == array->expand_rate + 1, "Should stay at the expand_rate at least.");
  62. DArray_contract(array);
  63. mu_assert((unsigned int)array->max == array->expand_rate + 1, "Should stay at the expand_rate at least.");
  64. return NULL;
  65. }
  66. char *test_push_pop()
  67. {
  68. int i = 0;
  69. for(i = 0; i < 1000; i++) {
  70. int *val = DArray_new(array);
  71. *val = i * 333;
  72. DArray_push(array, val);
  73. }
  74. mu_assert(array->max == 1201, "Wrong max size.");
  75. for(i = 999; i >= 0; i--) {
  76. int *val = DArray_pop(array);
  77. mu_assert(val != NULL, "Shouldn't get a NULL.");
  78. mu_assert(*val == i * 333, "Wrong value.");
  79. DArray_free(val);
  80. }
  81. return NULL;
  82. }
  83. char * all_tests() {
  84. mu_suite_start();
  85. mu_run_test(test_create);
  86. mu_run_test(test_new);
  87. mu_run_test(test_set);
  88. mu_run_test(test_get);
  89. mu_run_test(test_remove);
  90. mu_run_test(test_expand_contract);
  91. mu_run_test(test_push_pop);
  92. mu_run_test(test_destroy);
  93. return NULL;
  94. }
  95. RUN_TESTS(all_tests);

这向你展示了所有操作都如何使用,它会使DArray的实现变得容易:

  1. #include <lcthw/darray.h>
  2. #include <assert.h>
  3. DArray *DArray_create(size_t element_size, size_t initial_max)
  4. {
  5. DArray *array = malloc(sizeof(DArray));
  6. check_mem(array);
  7. array->max = initial_max;
  8. check(array->max > 0, "You must set an initial_max > 0.");
  9. array->contents = calloc(initial_max, sizeof(void *));
  10. check_mem(array->contents);
  11. array->end = 0;
  12. array->element_size = element_size;
  13. array->expand_rate = DEFAULT_EXPAND_RATE;
  14. return array;
  15. error:
  16. if(array) free(array);
  17. return NULL;
  18. }
  19. void DArray_clear(DArray *array)
  20. {
  21. int i = 0;
  22. if(array->element_size > 0) {
  23. for(i = 0; i < array->max; i++) {
  24. if(array->contents[i] != NULL) {
  25. free(array->contents[i]);
  26. }
  27. }
  28. }
  29. }
  30. static inline int DArray_resize(DArray *array, size_t newsize)
  31. {
  32. array->max = newsize;
  33. check(array->max > 0, "The newsize must be > 0.");
  34. void *contents = realloc(array->contents, array->max * sizeof(void *));
  35. // check contents and assume realloc doesn't harm the original on error
  36. check_mem(contents);
  37. array->contents = contents;
  38. return 0;
  39. error:
  40. return -1;
  41. }
  42. int DArray_expand(DArray *array)
  43. {
  44. size_t old_max = array->max;
  45. check(DArray_resize(array, array->max + array->expand_rate) == 0,
  46. "Failed to expand array to new size: %d",
  47. array->max + (int)array->expand_rate);
  48. memset(array->contents + old_max, 0, array->expand_rate + 1);
  49. return 0;
  50. error:
  51. return -1;
  52. }
  53. int DArray_contract(DArray *array)
  54. {
  55. int new_size = array->end < (int)array->expand_rate ? (int)array->expand_rate : array->end;
  56. return DArray_resize(array, new_size + 1);
  57. }
  58. void DArray_destroy(DArray *array)
  59. {
  60. if(array) {
  61. if(array->contents) free(array->contents);
  62. free(array);
  63. }
  64. }
  65. void DArray_clear_destroy(DArray *array)
  66. {
  67. DArray_clear(array);
  68. DArray_destroy(array);
  69. }
  70. int DArray_push(DArray *array, void *el)
  71. {
  72. array->contents[array->end] = el;
  73. array->end++;
  74. if(DArray_end(array) >= DArray_max(array)) {
  75. return DArray_expand(array);
  76. } else {
  77. return 0;
  78. }
  79. }
  80. void *DArray_pop(DArray *array)
  81. {
  82. check(array->end - 1 >= 0, "Attempt to pop from empty array.");
  83. void *el = DArray_remove(array, array->end - 1);
  84. array->end--;
  85. if(DArray_end(array) > (int)array->expand_rate && DArray_end(array) % array->expand_rate) {
  86. DArray_contract(array);
  87. }
  88. return el;
  89. error:
  90. return NULL;
  91. }

这占你展示了另一种处理复杂代码的方法,观察头文件并阅读单元测试,而不是一头扎进.c实现中。这种“具体的抽象”让你理解代码如何一起工作,并且更容易记住。

优点和缺点

DArray在你需要这些操作时占优势。

  • 迭代。你可以仅仅使用基本的for循环,使用DArray_countDArray_get来完成任务。不需要任何特殊的宏。并且由于不处理指针,它非常快。
  • 索引。你可以使用DArray_getDArray_set来随机访问任何元素,但是List上你就必须经过第N个元素来访问第N+1个元素。
  • 销毁。你只需要以两个操作销毁结构体和content。但是List需要一些列的free调用同时遍历每个元素。
  • 克隆。你只需要复制结构体和content,用两步复制整个结构。List需要遍历所有元素并且复制每个ListNode和值。
  • 排序。你已经见过了,如果你需要对数据排序,List非常麻烦。DArray上可以实现所有高效的排序算法,因为你可以随机访问任何元素。
  • 大量数据。如果你需要储存大量数据,DArray由于基于content,比起相同数量的ListNode占用更少空间而占优。

然而List在这些操作上占优势。

  • 在开头插入和移除元素。DArray需要特殊的优化来高效地完成它,并且通常还需要一些复制操作。
  • 分割和连接。List只需要复制一些指针就能完成,但是DArray需要复制涉及到的所有数组。
  • 少量数据。如果你只需要存储几个元素,通常使用List所需的空间要少于DArray,因为DArray需要考虑到日后的添加而扩展背后的空间,但是List只需要元素所需的空间。

考虑到这些,我更倾向使用DArray来完成其它人使用List所做的大部分事情。对于任何需要少量节点并且在两端插入删除的,我会使用List。我会想你展示两个相似的数据结构,叫做StackQueue,它们也很重要。

如何改进

像往常一样,浏览每个函数和操作,并且执行防御性编程检查,以及添加先决条件、不变量等任何可以使实现更健壮的东西。

附加题

  • 改进单元测试来覆盖耕作操作,并使用for循环来测试迭代。
  • 研究DArray上如何实现冒泡排序和归并排序,但是不要马上实现它们。我会在下一张实现DArray的算法,之后你可以完成它。
  • 为一些常用的操作编写一些性能测试,并与List中的相同操作比较。你已经做过很多次了,但是这次需要编写重复执行所涉及操作的单元测试,之后在主运行器中计时。
  • 观察DArray_expand如何使用固定增长(size + 300)来实现。通常动态数组都以倍数增长(size * 2)的方式实现,但是我发现它会花费无用的内存并且没有真正取得性能收益。测试我的断言,并且看看什么情况下需要倍数增长而不是固定增长。