一、 前言
在C语言,数组、字符串等数据结构都有固定的大小和边界。如果我们在处理这些数据结构时不小心越过了边界,就会出现各种问题,如数组越界、字符串溢出等,可能会导致程序崩溃或出现安全漏洞。因此,正确处理边界条件是C语言编程中的一个关键点。哨兵思想是一种常用的边界处理技术。
二、 哨兵思想的介绍
哨兵思想的基本思路是:在数据结构的末尾添加一个特殊的"哨兵"元素,这个哨兵元素具有一些特殊的性质,可以用来判断是否已经到达了数据结构的边界。
这个"哨兵"元素通常具有以下特点:
- 特殊值: 哨兵元素通常是一个特殊的值,比如数字
-1
、字符'\0'
等。这个特殊值不会出现在正常的数据中。 - 边界标记: 通过检查是否遇到了哨兵元素,我们可以知道是否已经到达了数据结构的边界。
- 终止条件: 在遍历或处理数据结构时,我们可以一直遍历到遇到哨兵元素为止,这样就可以确保不会越过数据结构的边界。
我们来看一下下面这段代码
#include <stdio.h>
// 不使用哨兵的 find_index 函数
int find_index1(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
// 使用哨兵的 find_index 函数
int find_index2(int arr[], int n, int target) {
if (n == 0) {
return -1;
}
// 扩展数组并设置哨兵
int temp_arr[n + 1];
for (int i = 0; i < n; i++) {
temp_arr[i] = arr[i];
}
temp_arr[n] = target;
// 开始查找
int i = 0;
while (temp_arr[i] != target) {
i++;
}
// 判断结果
if (i == n) {
return -1;
} else {
return i;
}
}
int main() {
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3, 4, 5};
int target1 = 3;
int target2 = 6;
printf("find_index1(%d) = %d\n", target1, find_index1(arr1, 5, target1));
printf("find_index1(%d) = %d\n", target2, find_index1(arr1, 5, target2));
printf("find_index2(%d) = %d\n", target1, find_index2(arr2, 5, target1));
printf("find_index2(%d) = %d\n", target2, find_index2(arr2, 5, target2));
return 0;
}
我们分别来看一下find_index1
和find_index2
的处理流程图,从图中我们可以很清晰的看到使用哨兵模式后不需要每次循环都进行n>=5的边界判断,只需要在最后进行判断就好。
三、 哨兵思想的使用场景
- 字符串处理: 我们以 ‘\0’ 作为字符串的结尾标记。在处理字符串时,只要遇到 ‘\0’ 就知道已经到达了字符串的末尾。
- 数组处理: 在数组末尾添加一个特殊值作为哨兵元素,在遍历数组时一直检查是否遇到了这个哨兵值。
- 链表处理: 在链表的末尾添加一个特殊的"空"节点作为哨兵,在遍历链表时一直检查是否遇到了这个哨兵节点。
三、 哨兵思想的优缺点
优点:
1、哨兵基本不能降低数据结构相关操作的渐近时间界,但其可以降低常数因子。
2、哨兵的设计可以使得代码更简洁,可以省去一些由于边界环境不同而作出的特殊处理。
3、在某些情况下哨兵可以使得循环语句更紧凑,降低运行时间里n或者n²项的系数。
缺点:
哨兵会占用额外的内存。当使用哨兵的开销较大时,有可能会造成严重的浪费。