二分查找

转载自:http://www.cppblog.com/converse  


二分查找算法基本思想
二分查找算法的前置条件是,一个已经排序好的序列(序列是升序排列的),这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止.

用伪代码来表示, 二分查找算法大致是这个样子的:
left = 0, right = n -1
while (left <= right)
    mid = (left + right) / 2
    case
        x[mid] < t:    left = mid + 1;
        x[mid] = t:    p = mid; break;
        x[mid] > t:    right = mid -1;

return -1;


第一个正确的程序
根据前面给出的算法思想和伪代码, 我们给出第一个正确的程序,但是,它还有一些小的问题,后面会讲到
int search(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;//可能会出现溢出
        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}


下面,讲讲在编写二分查找算法时可能出现的一些问题.

边界错误造成的问题
二分查找算法的边界,一般来说分两种情况,一种是左闭右开区间,类似于[left, right),一种是左闭右闭区间,类似于[left, right].需要注意的是, 循环体外的初始化条件,与循环体内的迭代步骤, 都必须遵守一致的区间规则,也就是说,如果循环体初始化时,是以左闭右开区间为边界的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误.比如下面就是错误的二分查找算法:
int search_bad(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n;

    while (left < right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle - 1;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

这个算法的错误在于, 在循环初始化的时候,初始化right=n,也就是采用的是左闭右开区间,而当满足array[middle] > v的条件是, v如果存在的话应该在[left, middle)区间中,但是这里却把right赋值为middle - 1了,这样,如果恰巧middle-1就是查找的元素,那么就会找不到这个元素.
下面给出两个算法, 分别是正确的左闭右闭和左闭右开区间算法,可以与上面的进行比较:
int search2(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle - 1;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

int search3(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n;

    while (left < right)
    {
        middle = (left + right) / 2;

        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle + 1;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}


死循环
上面的情况还只是把边界的其中一个写错, 也就是右边的边界值写错, 如果两者同时都写错的话,可能会造成死循环,比如下面的这个程序:
int search_bad2(int array[], int n, int v)
{
    int left, right, middle;

    left = 0, right = n - 1;

    while (left <= right)
    {
        middle = (left + right) / 2;
        if (array[middle] > v)
        {
            right = middle;
        }
        else if (array[middle] < v)
        {
            left = middle;
        }
        else
        {
            return middle;
        }
    }

    return -1;
}

这个程序采用的是左闭右闭的区间.但是,当array[middle] > v的时候,那么下一次查找的区间应该为[middle + 1, right], 而这里变成了[middle, right];当array[middle] < v的时候,那么下一次查找的区间应该为[left, middle - 1], 而这里变成了[left, middle].两个边界的选择都出现了问题, 因此,有可能出现某次查找时始终在这两个范围中轮换,造成了程序的死循环.

溢出
前面解决了边界选择时可能出现的问题, 下面来解决另一个问题,其实这个问题严格的说不属于算法问题,不过我注意到很多地方都没有提到,我觉得还是提一下比较好.
在循环体内,计算中间位置的时候,使用的是这个表达式:
middle = (left + right) / 2;

假如,left与right之和超过了所在类型的表示范围的话,那么middle就不会得到正确的值.
所以,更稳妥的做法应该是这样的:
middle = left + (right - left) / 2;

更完善的算法
前面我们说了,给出的第一个算法是一个"正确"的程序, 但是还有一些小的问题.
首先, 如果序列中有多个相同的元素时,查找的时候不见得每次都会返回第一个元素的位置, 比如考虑一种极端情况:序列中都只有一个相同的元素,那么去查找这个元素时,显然返回的是中间元素的位置.
其次, 前面给出的算法中,每次循环体中都有三次情况,两次比较,有没有办法减少比较的数量进一步的优化程序?
<<编程珠玑>>中给出了解决这两个问题的算法,结合前面提到溢出问题我对middle的计算也做了修改:
int search4(int array[], int n, int v)
{
    int left, right, middle;

    left = -1, right = n;

    while (left + 1 != right)
    {
        middle = left + (right - left) / 2;

        if (array[middle] < v)
        {
            left = middle;
        }
        else
        {
            right = middle;
        }
    }

    if (right >= n || array[right] != v)
    {
        right = -1;
    }

    return right;
}

这个算法是所有这里给出的算法中最完善的一个,正确,精确且效率高.
参考资料
1.<<编程珠玑>>
2.wiki上关于二分查找的说明:http://en.wikipedia.org/wiki/Binary_search
<think>好的,用户问的是常量变量的命名惯例和约定,除了基本的命名规则之外。首先,我需要回忆一下常见的编程命名规范,比如不同语言中的习惯,以及一些通用的最佳实践。 首先想到的是命名风格,比如驼峰式、蛇形式等。可能常量变量在风格上有不同的惯例。例如,在Java中常量通常用全大写加下划线,而变量用驼峰式。Python可能用蛇形大写表示常量变量用蛇形小写。需要确认这些不同语言的惯例。 然后,可能需要提到可读性,即命名要有意义,避免缩写或单字母变量,除非在特定上下文中有明确意义,比如循环中的i、j。这时候需要举例子说明好的和不好的命名方式。 接下来是作用域相关的命名,比如全局变量可能需要更详细的名称,局部变量可以简短。但这也取决于语言和项目规范。例如,全局变量可能加前缀,或者模块名前缀,以避免冲突。 还有特定领域的惯例,比如数学公式中的变量名保持与公式一致,比如用x、y表示坐标,i、j表示索引。在科学计算中可能保留传统的变量名,如T表示温度,p表示压力等。 另外,可能需要提到避免保留字或内置函数名,比如在Python中不要用“str”或“list”作为变量名。这属于命名冲突的问题。 还有常量是否应该用不可变的数据结构,比如在JavaScript中用const声明,但这是声明方式,可能不属于命名惯例,不过有时命名和声明方式相关。 可能还要注意语言特有的规范,比如Go语言推荐短名称,特别是作用域小的变量,而Ruby可能更偏向于蛇形小写。这些差异需要简要说明。 另外,常量的不变性,在命名时强调其不可变,比如用MAX_VALUE,而变量用currentValue。这时候命名可以传达其用途和可变性。 还有避免误导性名称,比如一个名为“userList”的变量实际上不是列表类型,而可能是一个字典,这会引起混淆。所以命名要和实际类型或用途一致。 可能需要补充团队协作中的一致性,遵循项目已有的命名风格,即使与个人习惯不同,也要保持统一。这也是重要的实践。 总结下来,应该分点说明,包括命名风格、可读性、作用域、特定领域惯例、避免保留字、语言特定规范、避免误导、团队一致性等。需要结构清晰,用例子帮助理解,同时注意用户可能想知道不同语言中的差异,但如果没有特别说明语言,应该保持通用,同时举几个常见语言的例子。</think>除了命名规则(如字母/数字/下划线组合、避免保留字等),常量变量还需遵循以下命名惯例: --- ### **一、命名风格惯例** 1. **常量**(值不变) - 全大写 + 下划线分隔,明确表示不可变性 ```python MAX_LENGTH = 100 # Python常量(惯例,非语法强制) const MAX_RETRIES = 3; // JavaScript ``` - 若为全局常量,可添加模块前缀: ```java public static final int MATH_PI_DIGITS = 6; // Java ``` 2. **变量**(值可变) - **驼峰式**(C/C++/Java/JavaScript等): ```java int currentScore; // 小驼峰 String FileName; // 大驼峰(类/类型名更常见) ``` - **蛇形式**(Python/Ruby等): ```python user_age = 25 # 小写 + 下划线 ``` --- ### **二、可读性要求** 1. **语义明确** - 避免单字母命名(除非作用域极小,如循环变量`i`) ✅ `for (int i = 0; i < 10; i++)` ❌ `int a = getData()` - 优先完整单词,避免生僻缩写 ✅ `calculateTax()` ❌ `calcTx()` 2. **避免误导** - 类型一致性:变量名应与实际类型匹配 ✅ `isValid: boolean` ❌ `isValid: int`(用 0/1 表示布尔值易混淆) --- ### **三、作用域相关** 1. **全局变量** - 添加模块/功能前缀,避免冲突 ```c extern int g_configTimeout; // &#39;g_&#39; 表示全局(C语言惯例) ``` 2. **局部变量** - 简短但清晰,如循环变量用 `i, j`,临时值用 `temp` --- ### **四、领域特定惯例** 1. **数学/科学计算** - 保留传统符号(如 `x, y` 表示坐标,`t` 表示时间) ```python velocity_x = 3.0 # 符合物理学惯例 ``` 2. **数据库交互** - 变量名与字段名对齐 ```java String dbUserName = resultSet.getString("user_name"); ``` --- ### **五、语言特性差异** | 语言 | 常量惯例 | 变量惯例 | |---------|------------------|------------------| | Python | 全大写蛇形 (`MAX_SIZE`) | 小写蛇形 (`user_name`) | | Java | 全大写蛇形 (`FINAL_VALUE`) | 小驼峰 (`currentValue`) | | C++ | 全大写蛇形 (`DEFAULT_COLOR`) | 小驼峰或蛇形 | | JavaScript | `const` + 驼峰 (`const maxItems`) | 驼峰 (`itemCount`) | --- ### **六、其他最佳实践** 1. **避免保留字冲突** - 错误:`int class = 5;`(`class` 是关键字) - 修正:`int studentClass = 5;` 2. **团队一致性** - 遵循项目现有风格(即使与个人习惯不同) 3. **匈牙利命名法(视场景)** - 类型前缀(较少用,但在WinAPI中可见): ```c int iCounter; // &#39;i&#39; 表示整型 char szName[20]; // &#39;sz&#39; 表示以零结尾的字符串 ``` --- 通过以上惯例,代码的可维护性和可读性会显著提高,尤其在团队协作和长期项目中更为关键。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值