一文彻底搞懂二分算法的细节

朴素二分

        当数组有序时,我们可以用二分索引一个值。left指针在数组左边,right指针在数组右边,mid是数组的中间值。

        mid = left + (right - left) / 2 ------->  mid永远都是中间值。

        加入数据是升序,当mid索引到的数据大于或小于mid时,可以让left = mid + 1right = mid -1 。每次索引都能排除当前剩余数据的一半,举个例子,如果有2亿数据量,一次索引就可以排除1亿数据量。这种算法的事件复杂度是 O(logN) ,相较于 O(N),它可以指数级的降低时间复杂度。

        来看一道题-------->704. 二分查找 - 力扣(LeetCode)

代码示例

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
            int m = left + (right - left) / 2;
            if (nums[m] < target)  left = m + 1;
            else if (nums[m] > target) right = m - 1;
            else return m;
        }
        return -1;
    }
};

求中间值 mid 时,下述写法可以防止 mid 溢出,这时二分查找的细节之一。

left + (right - left) / 2

非朴素二分

        非朴素二分是在朴素二分的思想上进行泛化,判断一道题是否需要二分算法不在是“数组是否有序” 这样的看法,而是 “数组有没有二段性”  。二段性是指可以用某个条件把数组区分成不同的两段。

       来看一道题------->34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

        题目描述的 非递减顺序 是指数据可以递增也可以不变。target 作为目标值,让我们找一段值为target的区间

        区间是一段数据,听上去好像不能用二分查找,因为二分查找只能找一个数据。我们可以用两次二分分别找到区间的左端点区间的右端点即可。这种算法的时间复杂度依然是 O(logN)。

        用二分算法分别找到区间的左端点区间的右端点 听上去简单,其实细节非常多。下面我以这道题为例分析 “用二分算法分别找到区间的左端点 ”的细节

细节分析

示例:

nums = [5,7,7,8,8,10], target = 8

指针初始化:left = 0 , right = nums.size() - 1;

mid 在数组中有三种命中情况

1. 命中区间的左边:nums[mid] < target

2.命中区间的右边:nums[mid] > target

3.命中区间某个值:nums[mid] = target

把情况2 和 3 合并,数组一定不是降序,我们可以分析其中的二段性

1. nums[mid] < target:left ~ mid 这个区间一定是全都小于target。那么我们应该去 mid + 1 ~ right 这个区间里寻找区间的左端点,如下图示意

2.nums[mid] >= target:right ~ mid 区间全部大于大于或等于target ,那么我们应该去 mid  ~ left 这个区间里寻找区间的左端点,为什么不是 mid - 1 ~ left呢? 因为mid 可能刚好是区间的左端点,如下图示意。

这里总结第一个细节

nums[mid] < target :left = mid + 1

nums[mid] >= target :right = mid;

第二个细节求中点的值,有两种写法

left + (right - left) / 2

left + (right - left + 1) / 2

        当数组长度为偶数时,有两个中间值(有两个mid),第一种写法mid会命中前一个中间值(靠近left),第二种写法mid会命中后一个中间值(靠近right)。我们在查找区间左端点时,应该用第一种写法,left + (right - left) / 2不会命中right,因为我们更新right下标时是right = mid,如果让mid命中right下标就会一直right = mid 循环下去。

第三个细节,就是出循环的判断条件,有两种写法

left < right 

left <= right 

我们选用第一种写法,因为left <= right 可能会死循环,left = right进入循环,nums[mid] >= target条件成立,更新下标right = mid,还会再次陷入循环。

查找区间右端点也类似,这里先给出代码示例

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        std::vector<int> ret;
        int len = nums.size();
        if (len == 0) return {-1, -1};
        int left = 0, right = len -1, m = 0;
        //查找区间左端点
        while (left < right) {
            m = left + (right - left) / 2;
            if (nums[m] < target) left = m + 1;
            else right = m;
        }
        if (nums[left] == target) ret.push_back(left);
        
        //查找区间右端点
        left = 0; right = len - 1;
        while (left < right) {
            m = left + (right - left + 1) / 2;
            if (nums[m] <= target) left = m;
            else right = m -1;
        }
        if (nums[left] == target) ret.push_back(left);
        else return {-1, -1};

        return ret;
    }
};

模板总结

查找区间左端点

 循环条件:left < right

下标更新:if (...) left = mid + 1 ;  if (...) right = mid;

中点值mid:left + (right - left) / 2

查找区间左端点

 循环条件:left < right

下标更新:if (...) left = mid;  if (...) right = mid - 1;

中点值mid:left + (right - left + 1) / 2

模板的思想总结

        当下标更新为if (...) left = mid + 1 ;  if (...) right = mid; 时,说明左区间完全不符合条件,右区间有可能符合条件,所以mid + 1表示left想跳出完全不符合条件的区间, right = mid 说明right尽可能地不漏掉符合条件地数据然后缩小查找区间(left ~ right)。

        查找区间右端点也是如此。

        而中点值地选择要看下标是如何更新的。如果是left = mid + 1,mid应该靠左,所以选择left + (right - left) / 2,如果是 right = mid - 1,mid应该靠右,所以选择left + (right - left + 1) / 2。

题目练习

下面都可以用非朴素二分算法结题

35. 搜索插入位置 - 力扣(LeetCode)

69. x 的平方根 - 力扣(LeetCode)

852. 山脉数组的峰顶索引 - 力扣(LeetCode)

162. 寻找峰值 - 力扣(LeetCode)

153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

LCR 173. 点名 - 力扣(LeetCode)

### 推荐算法概述 推荐系统旨在预测用户的兴趣并向用户提供个性化的建议。这类系统广泛应用于电子商务、社交媒体以及娱乐行业等领域,帮助用户发现感兴趣的商品或内容。为了构建有效的推荐引擎,通常采用三种主要类型的推荐技术:基于内容的过滤(Content-based Filtering),协同过滤(Collaborative Filtering),混合模型(Hybrid Models)[^4]。 #### 协同过滤原理 在众多推荐算法中,协同过滤是最常用的一种方法之一。其核心思想在于利用大量其他用户的行为数据来进行个性化推荐。具体来说,如果两个用户在过去表现出相似的兴趣偏好,则认为他们在未来也会有类似的喜好;同样地,对于同一类商品而言,被一群具有相同品味的人所喜爱意味着这些产品之间存在关联性[^4]。 ##### 用户-项目矩阵(U-V矩阵) 以视频平台为例,可以建立一个二维表格形式的关系结构——用户-视频矩阵(User-to-Item Matrix),其中每一行代表一位特定观众的历史观看记录,而每列则对应不同影片的信息。当面对稀疏的数据集时,可以通过填充缺失值或者仅保留评分较高的条目等方式简化处理过程。 ##### 计算相似度 针对上述提到的两种情况(即寻找相似用户和查找相近物品),需要定义合适的距离度量方式来量化彼此间的差异程度。常见的衡量标准包括余弦相似度(Cosine Similarity)、皮尔逊相关系数(Pearson Correlation Coefficient)等。例如,在计算两部电影之间的相似度时,可以选择后者作为评价指标: \[ \text{similarity}(A, B)=\frac{\sum_{i=1}^{n}\left(r_{Ai}-\bar{r}_{A}\right)\left(r_{Bi}-\bar{r}_{B}\right)}{\sqrt{\sum_{i=1}^{n}\left(r_{Ai}-\bar{r}_{A}\right)^{2}} \cdot \sqrt{\sum_{i=1}^{n}\left(r_{Bi}-\bar{r}_{B}\right)^{2}}} \] 这里 \( r_{Xj} \) 表示第 j 位用户给定 X 物品打下的分数,\( \overline {r_X } \) 则表示所有涉及此项目的平均得分。 ```python import numpy as np from scipy.spatial.distance import pdist, squareform def pearson_corr_matrix(data): """Calculate the Pearson correlation coefficient matrix.""" corr = 1 - squareform(pdist(data.T, metric='correlation')) return corr ``` #### 实现步骤 尽管具体的实施方案可能因应用场景的不同有所变化,但总体上遵循以下几个原则: - **数据预处理**:清洗并整理原始日志文件中的交互事件; - **特征提取**:根据业务需求选取恰当维度描述实体属性; - **模型训练**:运用机器学习框架完成参数估计工作; - **效果评估**:借助离线测试集验证性能表现,并持续迭代优化方案。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值