assert函数_Rust标准库中的BinaryHeap中Push函数源码解读

Struct std::collections::BinaryHeap

1 Rust std中的定义

A priority queue implemented with a binary heap. 由二进制堆实现的优先队列

This will be a max-heap. 大顶堆,根节点为最大值

其内部元素必须是可排序: 实现了Ord trait

2 使用方法

use std::collections::BinaryHeap;
// 创建一个BinaryHeap实例
let mut heap = BinaryHeap::new();

// 用push方法向堆内部依次输入三个数据:1,5,2
heap.push(1);
heap.push(5);
heap.push(2);

// 用peek()函数获取堆中的最大值,返回类型是Option<&T>
assert_eq!(heap.peek(), Some(&5));

// 用len()函数返回堆中的数据个数
assert_eq!(heap.len(), 3);

// 通过循环遍历堆,但堆输出的结果是乱序的
for x in &heap {
    println!("{}", x);
}

// 使用pop函数依次弹出堆中的数据,可见最大值在前,堆中已经默认排序好了我们输入的数字
assert_eq!(heap.pop(), Some(5));
assert_eq!(heap.pop(), Some(2));
assert_eq!(heap.pop(), Some(1));
assert_eq!(heap.pop(), None);

// 调用clear()函数清空整个堆
heap.clear();

// 调用is_empty()函数确认堆是否为空
assert!(heap.is_empty())

3 BinaryHeap中的Push函数源码解读

BinaryHeap数据结构的最大特点是在我们使用push函数往堆中添加值的时候,一旦输入一个较大的值,它会同当前堆中的其他值比较,然后不断“上浮”。

因此源码解读的重点就在于push函数的实现:

我们先来看BinaryHeap本身的数据结构:

#[stable(feature = "rust1", since = "1.0.0")]
pub struct BinaryHeap<T> {
    data: Vec<T>,
}

其本身比较简单,就是包裹了一个Vec<T> 然后我们来看看它的impl实现:

// 这里我们可以清晰地看到,BinaryHeap要求泛型实现Ord的trait bound
// Ord指的是Total Order,意味着我们往堆中添加的数据类型必须满足如下条件:
// 反对称性(Antisymmetry):a <= b 且 a >= b 可推出 a == b
// 传递性(Transitivity):a <= b 且 b <= c 可推出 a <= c
// 连通性(Connexity):a <= b 或 a >= b
impl<T: Ord> BinaryHeap<T>{
  // new() 函数也很简单,就是预先分配一个vec可变数组
    pub fn new() -> BinaryHeap<T> {
        BinaryHeap { data: vec![] }
    }
}

接下来我们重点需要看看push函数的实现:

pub fn push(&mut self, item: T) {
  // 形参item就是我们从外部要往堆中添加的数据
  // 在外部变量还未入堆前,先获取当前堆的长度(所含元素个数),并赋值给old_len变量
	let old_len = self.len(); 
  // 数据添加本质还是vec.push动作,往一个可变vector里面添加一个数据
	self.data.push(item);
  // 关键在于这里的sift_up()上浮函数,传入两个参数,一个是0,一个是数据未添加前的堆长度
	self.sift_up(0, old_len);
}

那么我们再来看看sift_up函数是怎么实现的:

// 很不幸这段代码,又牵出了另外几个我们不太熟悉概念:
fn sift_up(&mut self, start: usize, pos: usize) -> usize {
  unsafe {
    let mut hole = Hole::new(&mut self.data, pos);
    while hole.pos() > start {
      let parent = (hole.pos() - 1) / 2;
      if hole.element() <= hole.get(parent) {
        break;
      }
      hole.move_to(parent);
    }
    hole.pos()
  }
}

7a278cb410efd73b5174185b576f3cb7.png

那我们先来看看Hole到底是什么:

// Hole结构体从整体作用上来讲,将当前heap.data最后一个位置视作缺口。
// 而指向这个缺口的指针被暂时保存在ManuallyDrop<T>类型的成员变量elt中,缺口的下标值被保存在成员变量pos中
// 如果实例化后的Hole变量调用了move_to函数,那么pos的值会发生改变,意味着老缺口被填上新缺口诞生
// 当实例化后Hole生命周期结束,调用drop函数时,会将旧缺口指向的值,赋值给新缺口
// Hole结构体包含三个成员变量:一个数组切片slice,一个ManuallyDrop包裹的数据,一个usize类型的位置坐标
// ManuallyDrop正如其名,作用是阻止编译器自动地调用析构函数
pub struct ManuallyDrop<T: ?Sized> {
    value: T,
}
struct Hole<'a, T: 'a> {
    // data就是我们传递进来的数组切片,即为&heap.data
    data: &'a mut [T],
    elt: ManuallyDrop<T>,
    pos: usize,
}

impl<'a, T> Hole<'a, T> {
    #[inline]
    // 假设当前我们的heap.data的vec中有已经存入了[1,5],当我们再次push(2)时
    // heap.data = [1,5,2], old_len = 2 => heap.sift_up(0, 2)
    // => let mut hole = Hole::new(&heap, 2);
    unsafe fn new(data: &'a mut [T], pos: usize) -> Self {
        // 先确认是否当前当前heap的长度 > pos
        debug_assert!(pos < data.len());
        // ptr::read的作用就是从一个原始*const指针中读取指针指向的数据
        //  let x = 12; // x = 12
        //  let y = &x as *const i32; // y设定为指针类型存放x的地址
        //  unsafe {
        //      assert_eq!(std::ptr::read(y), 12); // 相当于*y
        //  }
        // get_unchecked是无视边界检查,直接从一个slice中通过下标读取数据
        // unsafe fn get_unchecked(self, slice: *const T) -> *const Self::Output
        // 根据get_unchecked函数原型可知其同样返回的是一个*const指针
        // 这样一来相当于把我们刚压入堆的数据存放到了Hole结构体的elt变量当中
        let elt = unsafe { ptr::read(data.get_unchecked(pos)) };
        // 实例化Hole结构体
        // Hole {data: &heap{1,5,2}, elt:ManuallyDrop{value:2}, pos:2 }
        Hole { data, elt: ManuallyDrop::new(elt), pos }
    }
    // 直接返回pos成员变量
    #[inline]
    fn pos(&self) -> usize {
        self.pos
    }

    // 返回elt成员变量的引用
    #[inline]
    fn element(&self) -> &T {
        &self.elt
    }

    // 仍然通过get_unchecked函数无视边界检查,直接返回slice中index下标所指向位置的数据
    // 但注意其返回类型是一个&T,也就是返回的是该数据的引用
    #[inline]
    unsafe fn get(&self, index: usize) -> &T {
        debug_assert!(index != self.pos);
        debug_assert!(index < self.data.len());
        unsafe { self.data.get_unchecked(index) }
    }

    // 这是Hole结构体最关键的函数
    // 我们将heap[index]的数据拷贝到heap[pos]中,也就是我们的缺口中
    #[inline]
    unsafe fn move_to(&mut self, index: usize) {
        // 先做两个检查:
        // 1 首先pos不能等于index,原地打转没有意义
        debug_assert!(index != self.pos);
        // 2 当前slice的长度必须大于index,其实也就是不能越界
        debug_assert!(index < self.data.len());
        unsafe {
            // 通过get_unchecked获取指向heap[index]的指针
            let index_ptr: *const _ = self.data.get_unchecked(index);
            // 通过get_unchecked_mut指向heap[pos]的指针,注意这里是mut说明该指针指向的内容是可变的
            let hole_ptr = self.data.get_unchecked_mut(self.pos);
            // 指针地址拷贝,从src拷贝到dst,src指针必须是不可变的,而dst是可变的,count是拷贝值
            // copy_nonoverlapping函数原型如下:
            // copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: usize)
            // 从src拷贝 count * size_of::<T>() 个字节到dst,不会管T是不是实现了Copy trait
            // dst和src指向的内容必须不能重叠,语义上C语言中的memcpy
            // 这里相当于从heap[index]位置的数据拷贝到heap[pos]
            ptr::copy_nonoverlapping(index_ptr, hole_ptr, 1);
        }
        // 更新Hole结构体中的pos,变为我们传递进来的index,构成了一个新的缺口
        self.pos = index;
    }
}
// 为Hole结构体编写的drop函数
impl<T> Drop for Hole<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            let pos = self.pos;
            // 仍然使用copy_nonoverlapping将我们保存在elt中的缺口值,赋值到heap[pos]位置
            // 如果我们调用过move_to函数,那么结构体中pos值会发生改变,以下代码的作用:
            // 相当于将move_to之前heap[旧pos]所指向的值,复制到heap[新pos],相当于把缺口填上
            ptr::copy_nonoverlapping(&*self.elt, self.data.get_unchecked_mut(pos), 1);
        }
    }
}

到此为止,基本原理都函数作用都已经讲完了,现在让我们来重新一步步审视下sift_up函数

我们从空heap开始,假设现在heap.data = ver![] 仍然是一个空数组

外部第一次调用:heap.push(1)

那么heap.push函数内部:heap.data = vec![1]; old_len = 0; => heap.sift_up(0, 0)

// sift_up(start = 0, pos = 0)
fn sift_up(&mut self, start: usize, pos: usize) -> usize {
  unsafe {
    // hole = {data:&[1],elt{value:1},pos:0}
    let mut hole = Hole::new(&mut self.data, pos);
    // start = 0, hole.pos() = 0 不满足循环条件,直接跳过
    while hole.pos() > start {
      let parent = (hole.pos() - 1) / 2;
      if hole.element() <= hole.get(parent) {
        break;
      }
      hole.move_to(parent);
    }
    // 返回 hole.pos() = 0
    hole.pos()
  }
}

外部第二次调用:heap.push(5)

那么heap.push函数内部:heap.data = vec![1,5]; old_len = 1; => heap.sift_up(0, 1)

// sift_up(start = 0, pos = 1)
fn sift_up(&mut self, start: usize, pos: usize) -> usize {
  unsafe {
    // hole = {data:&[1,5],elt{value:5},pos:1}
    let mut hole = Hole::new(&mut self.data, pos);
    // hole.pos() = 1 > start = 0 满足循环条件
    while hole.pos() > start {
      // parent = (1 - 1) / 2 = 0
      let parent = (hole.pos() - 1) / 2;
      // hole.element() = heap[1] = 5 > hole.get(0) = heap[0] = 1
      // 不满足分支判断,需要进行move_to操作
      if hole.element() <= hole.get(parent) {
        break;
      }
      // move_to(index = 0) => move_to做了两个操作
      // 1 将heap[index=0] = 1, 拷贝到 heap[pos=1]中,此时heap=[1,1]
      // 2 令hole.pos = index = 0
      hole.move_to(parent);
    }
    // 返回 hole.pos() = 0
    hole.pos() // 于是第二次进入while循环时,pos 已经不满足大于start的条件,结束循环
  }
  // 这里hole实例变量的生命周期结束,调用drop函数,重新将elt中保存的数据赋值到heap[pos]位置
  // 注意 此时由于调用过move_to函数pos已经改变为0,所以heap[0] = elt = 5
  // 最终当前heap变为[5,1]
}

外部第三次调用:heap.push(2)

那么heap.push函数内部:heap.data = vec![5,1,2]; old_len = 2; => heap.sift_up(0, 2)

// sift_up(start = 0, pos = 2)
fn sift_up(&mut self, start: usize, pos: usize) -> usize {
  unsafe {
    // hole = {data:&[5,1,2],elt{value:2},pos:2}
    let mut hole = Hole::new(&mut self.data, pos);
    // hole.pos() = 2 > start = 0 满足循环条件
    while hole.pos() > start {
      // parent = (2 - 1) / 2 = 1 取整数
      let parent = (hole.pos() - 1) / 2;
      // hole.element() = elt = 2 > hole.get(1) = heap[1] = 1
      // 不满足分支判断,需要进行move_to操作
      if hole.element() <= hole.get(parent) {
        break;
      }
      // move_to(index = 1) => move_to做了两个操作
      // 1 将heap[index=1] = 1, 拷贝到 heap[pos=2]位置中,此时heap=[5,1,1]
      // 2 令hole.pos = index = 1
      hole.move_to(parent);
    }
    // 返回 hole.pos() = 1
    hole.pos() 
    // 此时pos=1 仍然大于start=0,可以再次进入循环
    // 但是第二次循环时parent = 0,hole.element() =2 < hole.get(0) = 5 满足了跳出循环的分支判断
    // 不会执行后面的move_to,循环结束
  }
  // 这里hole实例变量的生命周期结束,调用drop函数,重新将elt中保存的数据赋值到heap[pos]位置
  // 注意此时由于调用过move_to函数pos已经改变为1,所以heap[1] = elt = 2
  // 最终当前heap变为[5,2,1]
}

由此可见,sift_up函数其实是在以数组的1/2长度为距,进行不断地对比游走。其先将自己保存在一个elt当中,以自己的位置作为一个Hole缺口,然后以二分查找的模式不断地同前面的数值进行比较,如果比自己小,就把它放到自己的缺口Hole中,并以当前的位置作为新的缺口,继续查找对比下去,直到遇到比他大的数据为止。最终在Hole结构体生命周期结束前,将其elt中保存的自己,放入当前最新的缺口位置,排序完成。

按照Rust标准库中官方的说法:缺口替换的模式比单纯的数据交换来得更快:

The implementations of sift_up and sift_down use unsafe blocks in order to move an element out of the vector (leaving behind a hole), shift along the others and move the removed element back into the vector at the final location of the hole. The Hole type is used to represent this, and make sure the hole is filled back at the end of its scope, even on panic. Using a hole reduces the constant factor compared to using swaps, which involves twice as many moves.

4 应用:一道leetcode题目

给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。

你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。

请你返回你可以参加的最大会议数目。

示例 1:

输入:events = [[1,2],[2,3],[3,4]] 输出:3 解释:你可以参加所有的三个会议。 安排会议的一种方案如上图。 第 1 天参加第一个会议。 第 2 天参加第二个会议。 第 3 天参加第三个会议。 示例 2:

输入:events= [[1,2],[2,3],[3,4],[1,2]] 输出:4 示例 3:

输入:events = [[1,4],[4,4],[2,2],[3,4],[1,1]] 输出:4 示例 4:

输入:events = [[1,100000]] 输出:1 示例 5:

输入:events = [[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7]] 输出:7

以下是我写的垃圾代码,结构比较混乱,如果不写说明很难看懂:

/*
solution:
1 sort the vec , first sort by startday then the endday,and return the whole range of days of meeting =>
return a vec like this: 
if there are 7 days for holding the meeting, we can create the dayrange vec [usize;7] = [0,0,0,0,0,0,0]
1 note 1 as attending the meeting while 0 as not
2 iterate the whole vec of meetings
3 the first elemet of vec must be the attendable meeting, we note its startday on the dayrange vec as 1
4 iterate the next element of vec => if the startday is noted on the dayrange vec then look to the endday => if the endday is not noted then note it, while it is noted then end this iterator
5 sum the vec
*/
//-> (Vec<Vec<usize>>,usize)
fn get_meeting_number(meetings:&Vec<Vec<usize>>,rangedays:usize) -> usize{
    let mut i :usize = 0;
    let mut j :usize = 0;
    let mut maxattending :usize = 0;
    let mut rdarr :Vec<usize> = Vec::new();
    for a in 0..rangedays{
        rdarr.push(0);
    }
    for m in meetings{
        if i == 0 {
            rdarr[0] = 1;
        }else{
            if rdarr[m[0]-1] == 0{
                rdarr[m[0]-1] = 1;
            }else if  rdarr[m[1]-1] == 0{
                rdarr[m[1]-1] = 1;
            }else{
                continue;
            }
        }
        i+=1;
    }
    maxattending = rdarr.iter().sum();
    maxattending
}
fn sort_the_vec(meetings:&mut Vec<Vec<usize>>) ->usize {
    meetings.sort_by(|a,b|a[0].cmp(&b[0]));
    let mut i :usize = 0;
    let mut j :usize = 0;
    let mut lastday :usize = 0;
    if meetings.len() == 1{
        lastday = 1;
        return lastday;
    }
    while i < meetings.len(){
        
        j=i+1;
        while j < meetings.len() -1
        {
            if meetings[i][1] > meetings[j][1] && meetings[i][0] == meetings[j][0] {
            let tmp = meetings[i][1];
            meetings[i][1] = meetings[j][1];
            meetings[j][1] =tmp;
            }

            j+=1;
        }
        i+=1;
    }
    i=0;
    while i < meetings.len()-1{
        if meetings[i][1] > meetings[i+1][1]{
            lastday = meetings[i][1];
        }
        else{
            lastday=meetings[i+1][1];
        }
        i+=1;
    }
    println!("{:?}",meetings);
    lastday
}
pub fn run() {
      let mut meetings: Vec<Vec<usize>> = 
  vec![vec![1,1],vec![1,2],vec![1,3],vec![1,4],vec![1,5],vec![1,6],vec![1,7]];
    let rangedays = sort_the_vec(&mut meetings);
    let maxattending = get_meeting_number(&meetings,rangedays);
    println!("{}",maxattending);
}
// 输出结果:
// [[1, 1], [1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [1, 7]]
// 7

大神写的代码:将会议的起止日期打包为一个Range结构体,并且实现Eq、PartialEq、PartialOrd和Ord,然后将与会日期排序的工作交给BinaryHeap自行完成,将排序好的日期序列,一个个弹出,然后将与会标记挂到BTreeMap中。整体逻辑非常清晰。

use std::collections::BinaryHeap;
use std::cmp::Ordering;
use std::collections::BTreeMap;
#[derive(Debug,Clone)]
struct Range{
    start:i32,
    end:i32,
}
impl std::cmp::Eq for Range{}
impl std::cmp::PartialEq for Range{
    fn eq(&self,other:&Self) -> bool{
        self.cmp(other) == Ordering::Equal
    }
}
impl std::cmp::PartialOrd for Range{
    fn partial_cmp(&self,other:&Self) -> Option<Ordering>{
        Some(self.cmp(other))
    }
}
impl std::cmp::Ord for Range{
    fn cmp(&self,other:&Self) -> Ordering{
        return if other.end >  self.end{
            Ordering::Greater
        }else if other.end < self.end{
            Ordering::Less
        }else{
            let range1 = self.end - self.start;
            let range2 = other.end - other.start;
            if range2 > range1{
                Ordering::Greater
            }else if range2 < range1{
                Ordering::Less
            }else{
                if other.start > self.start{
                    Ordering::Greater
                }else if other.start < self.start{
                    Ordering::Less
                }else{
                    Ordering::Equal
                }
            }
        }
    }
}
struct Solution{}
impl Solution{
    pub fn max_events(events:Vec<Vec<i32>>) -> i32{
        let mut heap = BinaryHeap::new();
        for e in events{
            heap.push(Range{
                start: e[0],
                end:   e[1],
            });
        }
        println!("{:?}",heap);
        let mut m = BTreeMap::new();
        let mut count = 0;
        while let Some(r) = heap.pop(){
            println!("r={:?}",r);
            for i in r.start..r.end+1{
                use std::collections::btree_map::Entry;
                let e = m.entry(i);
                match e {
                    Entry::Occupied(_) => { continue;},
                    Entry::Vacant(o) => {
                        o.insert(1);
                        count+=1;
                        break;
                    } 
                }
            }
        }
        count
    }
}
#[cfg(test)]
mod test{
    use super::*;
    #[test]
    fn test() {
        let t = Solution::max_events(vec![vec![1, 3], vec![1, 3], vec![1, 3], vec![3, 4]]);
        assert_eq!(t, 4);
        // return;
        let t = Solution::max_events(vec![vec![1, 2], vec![2, 3], vec![3, 4]]);
        assert_eq!(t, 3);
        // return;
        let t = Solution::max_events(vec![vec![1, 2], vec![2, 3], vec![3, 4], vec![1, 2]]);
        assert_eq!(t, 4);
        let t = Solution::max_events(vec![
            vec![1, 4],
            vec![4, 4],
            vec![2, 2],
            vec![3, 4],
            vec![1, 1],
        ]);
        assert_eq!(t, 4);
        let t = Solution::max_events(vec![vec![1, 100000]]);
        assert_eq!(t, 1);
        let t = Solution::max_events(vec![
            vec![1, 1],
            vec![1, 2],
            vec![1, 3],
            vec![1, 4],
            vec![1, 5],
            vec![1, 6],
            vec![1, 7],
        ]);
        assert_eq!(t, 7);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值