羅劍鋒的C++實戰筆記學習(二):容器、算法庫、多線程

4、容器

1)、容器的通用特性

所有容器都具有的一個基本特性:它保存元素采用的是值(value)語義,也就是說,容器里存儲的是元素的拷貝、副本,而不是引用

容器操作元素的很大一塊成本就是值的拷貝。所以,如果元素比較大,或者非常多,那么操作時的拷貝開銷就會很高,性能也就不會太好

一個解決辦法是,盡量為元素實現轉移構造和轉移賦值函數,在加入容器的時候使用std::move()來轉移,減少元素復制的成本:

#include <iostream>
#include <vector>class Point {
public:Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {std::cout << "Point constructor called." << std::endl;}Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {std::cout << "Copy constructor called." << std::endl;}Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {std::cout << "Move constructor called." << std::endl;}private:int x_, y_;std::string data; // 假設這是一個昂貴的資源
};int main() {// 不使用移動語義,直接拷貝{Point p(1, 2);std::vector<Point> v;v.push_back(p); // 這里會調用拷貝構造函數std::cout << "---- After normal push_back ----" << std::endl;}// 使用移動語義{Point p(3, 4);std::vector<Point> v;v.push_back(std::move(p)); // 這里會調用移動構造函數std::cout << "---- After move semantics push_back ----" << std::endl;}return 0;
}

輸出:

Point constructor called.
Copy constructor called.
---- After normal push_back ----
Point constructor called.
Move constructor called.
---- After move semantics push_back ----

也可以使用C++11為容器新增加的emplace操作函數,它可以就地構造元素,免去了構造后再拷貝、轉移的成本,不但高效,而且用起來也很方便:

#include <iostream>
#include <vector>
#include <string>class Point {
public:Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {std::cout << "Point constructor called." << std::endl;}Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {std::cout << "Copy constructor called." << std::endl;}Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {std::cout << "Move constructor called." << std::endl;}private:int x_, y_;std::string data; // 假設這是一個昂貴的資源
};int main() {std::vector<Point> v;v.reserve(2);  // 預分配足夠的空間,避免內部擴展// 使用emplace_back直接在容器內構造對象v.emplace_back(1, 2); // 這里不會調用拷貝或移動構造函數v.emplace_back(3, 4);// 當std::vector需要擴容時,會觸發之前對象的移動構造函數v.emplace_back(5, 6);return 0;
}

輸出:

Point constructor called.
Point constructor called.
Point constructor called.
Move constructor called.
Move constructor called.

還可以在容器里存放元素的指針,來間接保存元素。這里建議使用智能指針unique_ptr/shared_ptr,讓它們幫你自動管理元素,一般情況下,shared_ptr是一個更好的選擇,它的共享語義與容器的值語義基本一致

2)、順序容器

順序容器就是數據結構里的線性表,一共有5種:array、vector、deque、list、forward_list

按照存儲結構,這5種容器又可以再細分成兩組

  • 連續存儲的數組:array、vector和deque
  • 指針結構的鏈表:list和forward_list

數組:

array和vector直接對應C的內置數組,內存布局與C完全兼容,所以是開銷最低、速度最快的容器它們兩個的區別在于容量能否動態增長。array是靜態數組,大小在初始化的時候就固定了,不能再容納更多的元素。而vector是動態數組,雖然初始化的時候設定了大小,但可以在后面隨需增長,容納任意數量的元素

#include <array>
#include <vector>int main() {std::array<int, 2> arr;assert(arr.size() == 2);std::vector<int> v(2);for (int i = 0; i < 10; i++) {v.emplace_back(i);}assert(v.size() == 12);return 0;
}

deque也是一種可以動態增長的數組,它和vector的區別是,它可以在兩端高效地插入刪除元素,而vector則只能用push_back在末端追加元素

#include <deque>int main() {std::deque<int> d;d.emplace_back(9); // 末端添加一個元素d.emplace_front(1); // 前端添加一個元素assert(d.size() == 2);return 0;
}

鏈表:

vector和deque里的元素因為是連續存儲的,所以在中間的插入刪除效率就很低,而list和forward_list是鏈表結構,插入刪除操作只需要調整指針,所以在任意位置的操作都很高效

鏈表的缺點是查找效率低,只能沿著指針順序訪問,這方面不如vector隨機訪問的效率高。list是雙向鏈表,可以向前或者向后遍歷,而forward_list是單向鏈表,只能向前遍歷,查找效率就更低了

鏈表結構比起數組結構還有一個缺點,就是存儲成本略高,因為必須要為每個元素附加一個或者兩個的指針,指向鏈表的前后節點

擴容機制:

vector/deque和list/forward_list都可以動態增長來容納更多的元素,但它們的內部擴容機制卻是不一樣的

當vector的容量到達上限的時候(capacity),它會再分配一塊兩倍大小的新內存,然后把舊元素拷貝或者移動過去。這個操作的成本是非常大的,所以,在使用vector的時候最好能夠預估容量,使用reserve提前分配足夠的空間,減少動態擴容的拷貝代價

deque、list會按照固定的步長(例如N個字節、一個節點)去增加容量。但在短時間內插入大量數據的時候就會頻繁分配內存,效果反而不如vector一次分配來得好

如何選擇:

如果沒有什么特殊需求,首選的容器就是array和vector,它們的速度最快、開銷最低,數組的形式也令它們最容易使用,搭配算法也可以實現快速的排序和查找

剩下的deque、list和forward_list則適合對插入刪除性能比較敏感的場合,如果還很在意空間開銷,那就只能選擇非鏈表的deque了

3)、有序容器

順序容器的特點是,元素的次序是由它插入的次序而決定的,訪問元素也就按照最初插入的順序。而有序容器則不同,它的元素在插入容器后就被按照某種規則自動排序,所以是有序的

標準庫里一共有四種有序容器:set/multiset和map/multimap(底層是通過紅黑樹實現)。有multi前綴的容器表示可以容納重復的key

在定義有序容器的時候必須要指定key的比較函數。只不過這個函數通常是默認的less,表示小于關系,不用特意寫出來

C++里的int、string等基本類型都支持比較排序,但很多自定義類型沒有默認的比較函數,需要重載<或者自定義模板參數

比如說有一個Point類,它是沒有大小概念的,但只要給它重載<操作符,就可以放進有序容器里了:

#include <iostream>
#include <set>class Point {
public:Point(int x, int y) : x_(x), y_(y) {}bool operator<(const Point &other) const {if (x_ != other.x_) {return x_ < other.x_;} else {return y_ < other.y_;}}friend std::ostream &operator<<(std::ostream &os, const Point &p) {os << "(" << p.x_ << ", " << p.y_ << ")";return os;}private:int x_;int y_;
};int main() {std::set<Point> points;points.emplace(7, 2);points.emplace(3, 5);for (const auto &point: points) {std::cout << point << std::endl;}return 0;
}

另一種方式是編寫專門的函數對象或者lambda表達式,然后在容器的模板參數里指定。這種方式更靈活,而且可以實現任意的排序準則:

#include <iostream>
#include <set>template<typename Iter>
void printRangeWithCommas(Iter begin, Iter end) {if (begin == end) return;for (Iter it = begin; it != end; ++it) {std::cout << *it;if (std::next(it) != end) {std::cout << ",";}}std::cout << "\n";
}int main() {std::set<int> s = {7, 3, 9};printRangeWithCommas(s.begin(), s.end()); // 調用函數打印集合,輸出: 3,7,9auto comp = [](auto a, auto b) {return a > b;};std::set<int, decltype(comp)> gs(comp);std::copy(s.begin(), s.end(), std::inserter(gs, gs.end()));printRangeWithCommas(gs.begin(), gs.end()); // 再次調用函數打印另一個集合,輸出: 9,7,3return 0;
}
4)、無序容器

無序容器也有四種,名字里也有set和map,只是加上了unordered(無序)前綴,分別是unordered_set/unordered_multiset、unordered_map/unordered_multimap

無序容器用法上與有序容器幾乎是一樣的,區別在于內部數據結構:它不是紅黑樹,而是散列表(也叫哈希表,hash table)

因為它采用散列表存儲數據,元素的位置取決于計算的散列值,沒有規律可言,所以就是無序的

#include <iostream>
#include <unordered_map>int main() {using map_type =std::unordered_map<int, std::string>;map_type dict;dict[1] = "one";dict.emplace(2, "two");dict[10] = "ten";for (auto &x: dict) { // 遍歷順序不確定,既不是插入順序,也不是大小序std::cout << x.first << "=>"<< x.second << ",";}return 0;
}

無序容器要求key具備兩個條件,一是可以計算hash值,二是能夠執行相等比較操作。第一個是因為散列表的要求,只有計算hash值才能放入散列表,第二個則是因為hash值可能會沖突,所以當hash值相同時,就要比較真正的key值

#include <iostream>
#include <unordered_set>class Point {
public:Point(int x, int y) : x_(x), y_(y) {}int getX() const { return x_; }int getY() const { return y_; }// 重載operator==用于比較bool operator==(const Point &other) const {return x_ == other.x_ && y_ == other.y_;}// 為了使用Point作為std::unordered_set或std::unordered_map的鍵,需要定義哈希函數struct Hash {std::size_t operator()(const Point &p) const noexcept {std::size_t h1 = std::hash<int>{}(p.getX());std::size_t h2 = std::hash<int>{}(p.getY());return h1 ^ (h2 << 1);}};// 重載operator<<以便可以直接輸出Point對象friend std::ostream &operator<<(std::ostream &os, const Point &p) {return os << "(" << p.x_ << ", " << p.y_ << ")";}private:int x_, y_;
};int main() {std::unordered_set<Point, Point::Hash> pointSet;pointSet.insert(Point(1, 2));pointSet.insert(Point(3, 4));// 嘗試插入重復的點,不會成功pointSet.insert(Point(1, 2));for (const auto &point: pointSet) {std::cout << point << std::endl;}return 0;
}
5)、小結
  1. 標準容器可以分為三大類,即順序容器、有序容器和無序容器
  2. 所有容器中最優先選擇的應該是array和vector,它們的速度最快,開銷最低
  3. list是鏈表結構,插入刪除的效率高,但查找效率低
  4. 有序容器是紅黑樹結構,對key自動排序,查找效率高,但有插入成本
  5. 無序容器是散列表結構,由hash值計算存儲位置,查找和插入的成本都很低
  6. 有序容器和無序容器都屬于關聯容器,元素有key的概念,操作元素實際上是在操作key,所以要定義對key的比較函數或者散列函數

5、算法庫

1)、迭代器

迭代器常用的函數如下:

  • begin()end():得到表示兩個端點的迭代器
  • distance():計算兩個迭代器之間的距離
  • advance():前進或者后退 N 步
  • next()prev():計算迭代器前后的某個位置
#include <array>int main() {std::array<int, 5> arr = {0, 1, 2, 3, 4}; // array靜態數組容器auto b = begin(arr); // 全局函數獲取迭代器,首端auto e = end(arr); // 全局函數獲取迭代器,末端assert(std::distance(b, e) == 5); // 迭代器的距離auto p = std::next(b); // 獲取下一個位置assert(std::distance(b, p) == 1); // 迭代器的距離assert(std::distance(p, b) == -1); // 反向計算迭代器的距離std::advance(p, 2); // 迭代器前進兩個位置,指向元素3assert(*p == 3);assert(p == std::prev(e, 2)); // 是末端迭代器的前兩個位置return 0;
}
2)、for_each
#include <iostream>
#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10};for (const auto &x: v) { // range for循環std::cout << x << ",";}std::cout << "\n";auto print = [](const auto &x) // 定義一個lambda表達式{std::cout << x << ",";};for_each(cbegin(v), cend(v), print); // for_each算法std::cout << "\n";for_each( // for_each算法,內部定義lambda表達式cbegin(v), cend(v), // 獲取常量迭代器[](const auto &x) // 匿名lambda表達式{std::cout << x << ",";});std::cout << "\n";return 0;
}
3)、排序算法

sort()是經典的快排算法,示例如下:

#include <iostream>
#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10};auto print = [](const auto &x) // lambda表達式輸出元素{std::cout << x << ",";};std::sort(begin(v), end(v)); // 快速排序for_each(cbegin(v), cend(v), print);return 0;
}

一些常見問題對應的算法:

  • 要求排序后仍然保持元素的相對順序,應該用stable_sort,它是穩定的
  • 選出前幾名(TopN),應該用partial_sort
  • 選出前幾名,但不要求再排出名次(BestN),應該用nth_element
  • 中位數(Median)、百分位數(Percentile),還是用nth_element
  • 按照某種規則把元素劃分成兩組,用partition
  • 第一名和最后一名,用minmax_element
#include <iostream>
#include <vector>template<typename Iter>
void printRangeWithCommas(const std::string &prefix, Iter begin, Iter end) {if (begin == end) return;std::cout << prefix;for (auto it = begin; it != end; ++it) {std::cout << (it == begin ? "" : ",") << *it;}std::cout << '\n';
}int main() {std::vector<int> v = {3, 5, 1, 7, 10};// top3std::partial_sort(begin(v), next(begin(v), 3), end(v)); // 取前3名printRangeWithCommas("top3: ", v.begin(), next(begin(v), 3));// best3std::nth_element(begin(v), next(begin(v), 3), end(v)); // 最好的3個printRangeWithCommas("best3: ", v.begin(), next(begin(v), 3));// medianauto mid_iter = // 中位數的位置next(begin(v), v.size() / 2);std::nth_element(begin(v), mid_iter, end(v)); // 排序得到中位數std::cout << "median: " << *mid_iter << std::endl;// partitionauto pos = std::partition( // 找出所有大于9的數begin(v), end(v), [](const auto &x) // 定義一個lambda表達式{return x > 9;});printRangeWithCommas("values > 9: ", v.begin(), pos);// min/maxauto [minIt, maxIt] = std::minmax_element( // 找出第一名和倒數第一cbegin(v), cend(v));std::cout << "min value: " << *minIt << ", max value: " << *maxIt << std::endl;return 0;
}

在使用這些排序算法時,對迭代器要求比較高,通常都是隨機訪問迭代器(minmax_element除外),所以最好在順序容器array/vector上調用

4)、查找算法

binary_search:在已經排好序的區間里執行二分查找,但它只返回一個bool值,告知元素是否存在

#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};std::sort(begin(v), end(v)); // 快速排序// 二分查找,只能確定元素在不在bool found = binary_search(cbegin(v), cend(v), 7);assert(found);return 0;
}

想要在已序容器上執行二分查找,要用算法lower_bound,它返回第一個大于或等于值的位置

#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};std::sort(begin(v), end(v));// 找到第一個>=7的位置auto pos = std::lower_bound(cbegin(v), cend(v), 7);bool found = (pos != cend(v)) && (*pos == 7); // 可能找不到,所以必須要判斷assert(found); // 7在容器里// 找到第一個>=9的位置pos = std::lower_bound(cbegin(v), cend(v), 9);found = (pos != cend(v)) && (*pos == 9); // 可能找不到,所以必須要判斷assert(!found); // 9不在容器里return 0;
}

lower_bound的返回值是一個迭代器,需要做判斷才能知道是否真的找到了。判斷的條件有兩個,一個是迭代器是否有效,另一個是迭代器的值是不是要找的值

upper_bound算法:返回第一個大于值的元素(也是在已序容器上執行二分查找)

對于有序容器set/map,就不需要調用這三個算法了,它們有等價的成員函數find/lower_bound/upper_bound,效果是一樣的

#include <iostream>
#include <set>int main() {std::multiset<int> s = {3, 5, 1, 7, 7, 7, 10, 99, 42};auto pos = s.find(7); // 二分查找,返回迭代器assert(pos != s.end());auto lower_pos = s.lower_bound(7); // 獲取區間的左端點auto upper_pos = s.upper_bound(7); // 獲取區間的右端點auto print = [](const auto &x) {std::cout << x << ",";};// 輸出7,7,7std::for_each(lower_pos, upper_pos, print);return 0;
}

標準庫里還有一些查找算法可以用于未排序的容器,這些算法以find和search命名,其中用于查找區間的find_first_of/find_end

#include <vector>
#include <array>int main() {std::vector<int> v = {1, 9, 11, 3, 5, 7};// 查找算法,找到第一個出現的位置auto pos = std::find(begin(v), end(v), 3);assert(pos != end(v));// 查找算法,用lambda判斷條件pos = std::find_if(begin(v), end(v),[](auto x) {return x % 2 == 0;});assert(pos == end(v));std::array<int, 2> arr = {3, 5};// 查找一個子區間pos = std::find_first_of(begin(v), end(v),begin(arr), end(arr));assert(pos != end(v));return 0;
}
5)、小結
  1. 算法是專門操作容器的函數,是一種智能for循環,它的最佳搭檔是lambda表達式
  2. 算法通過迭代器來間接操作容器,使用兩個端點指定操作范圍,迭代器決定了算法的能力
  3. for_each算法是for的替代品,以函數式編程替代了面向過程編程
  4. 有多種排序算法,最基本的是sort,但應該根據實際情況選擇其他更合適的算法,避免浪費
  5. 在已序容器上可以執行二分查找,應該使用的算法是lower_bound
  6. list/set/map提供了等價的排序、查找函數,更適應自己的數據結構
  7. find/search是通用的查找算法,效率不高,但不必排序也能使用

6、多線程

1)、僅調用一次

要先聲明一個once_flag類型的變量,最好是靜態、全局的(線程可見),作為初始化的標志。然后調用專門的call_once()函數,以函數式編程的方式,傳遞這個標志和初始化函數。這樣C++就會保證,即使多個線程重入call_once(),也只能有一個線程會成功運行初始化

#include <iostream>
#include <thread>int main() {static std::once_flag flag; // 全局的初始化標志auto f = []() {std::call_once(flag, // 僅一次調用,注意要傳flag[]() { // 匿名lambda,初始化函數,只會執行一次std::cout << "only once" << std::endl;});};// 使用vector管理線程,確保所有線程執行完畢后再退出mainstd::vector<std::thread> threads;for (int i = 0; i < 2; ++i) {threads.emplace_back(f);}// 等待所有線程完成for (std::thread &t: threads) {t.join();}return 0;
}
2)、線程局部存儲

有thread_local標記的變量在每個線程里都會有一個獨立的副本,是線程獨占的

#include <iostream>
#include <thread>int main() {thread_local int n = 0; // 線程局部存儲變量auto f = [&](int x) {n += x; // 使用線程局部變量,互不影響std::cout << n << std::endl;};// 使用vector管理線程,確保所有線程執行完畢后再退出mainstd::vector<std::thread> threads;threads.emplace_back(f, 10);threads.emplace_back(f, 20);// 等待所有線程完成for (std::thread &t: threads) {t.join();}return 0;
}

在程序執行后,可以看到兩個線程分別輸出了10和20,互不干擾

3)、原子變量

目前,C++只能讓一些最基本的類型原子化,比如atomic_int、atomic_long等。這些原子變量都是模板類atomic的特化形式,包裝了原始的類型,具有相同的接口,用起來和bool、int幾乎一模一樣,但卻是原子化的,多線程讀寫不會出錯

原子變量和原始的類型一個重要的區別是:原子變量禁用了拷貝構造函數,所以在初始化的時候不能用=的賦值形式,只能用圓括號或者花括

#include <iostream>
#include <thread>void incrementAtomicInt(std::atomic_int &counter, int numIterations) {for (int i = 0; i < numIterations; ++i) {++counter;}
}void incrementAtomicLong(std::atomic_long &counter, long numIterations) {for (long i = 0; i < numIterations; ++i) {counter += 10;}
}int main() {const int numThreads = 4;const int iterationsPerThread = 100;std::atomic_int x{0};std::atomic_long y{1000L};std::vector<std::thread> threads;// 對x進行遞增操作的線程for (int i = 0; i < numThreads; ++i) {threads.emplace_back(incrementAtomicInt, std::ref(x), iterationsPerThread);}// 對y進行遞增操作的線程for (int i = 0; i < numThreads; ++i) {threads.emplace_back(incrementAtomicLong, std::ref(y), iterationsPerThread / 10); // 注意調整迭代次數以匹配期望的增量}// 等待所有線程完成for (std::thread &t: threads) {t.join();}std::cout << "final value of x: " << x << std::endl;std::cout << "final value of y: " << y << std::endl;assert(x == numThreads * iterationsPerThread);assert(y == 1000L + numThreads * (iterationsPerThread / 10) * 10);return 0;
}

除了模擬整數運算,原子變量還有一些特殊的原子操作,比如store、load、fetch_add、fetch_sub、exchange、compare_exchange_weak/compare_exchange_strong以及CAS(Compare And Swap)操作

4)、線程

C++標準庫里有專門的線程類thread,使用它就可以簡單地創建線程,在名字空間std::this_thread里,還有yield()get_id()sleep_for()sleep_until()等幾個方便的管理函數

#include <iostream>
#include <chrono>
#include <thread>int main() {static std::atomic_flag flag{false};static std::atomic_int n;auto f = [&]() {auto value = flag.test_and_set(); // TAS檢查原子標志量if (value) {std::cout << "flag has been set." << std::endl;} else {std::cout << "set flag by " << std::this_thread::get_id() << std::endl; // 輸出線程id}n += 100; // 原子變量加法運算std::this_thread::sleep_for(std::chrono::milliseconds(n.load() * 10));std::cout << n << std::endl;};std::vector<std::thread> threads;for (int i = 0; i < 2; ++i) {threads.emplace_back(f);}for (std::thread &t: threads) {t.join();}return 0;
}

函數async()含義是異步運行一個任務,隱含的動作是啟動一個線程去執行,但不絕對保證立即啟動(也可以在第一個參數傳遞 std::launch::async,要求立即啟動線程)

#include <iostream>
#include <chrono>
#include <thread>
#include <future>int main() {auto task = [](auto x) {std::this_thread::sleep_for(std::chrono::milliseconds(x));std::cout << "sleep for " << x << std::endl;return x;};auto f = std::async(task, 10);// 啟動一個異步任務f.wait(); // 等待任務完成assert(f.valid());// 確實已經完成了任務std::cout << f.get() << std::endl; // 獲取任務的執行結果return 0;
}

async()會返回一個future變量,如果任務有返回值,就可以用成員函數get()獲取。不過要特別注意,get()只能調一次,再次獲取結果會發生錯誤,拋出異常 std::future_error

這里還有一個很隱蔽的坑,如果不顯式獲取async()的返回值(即future對象),它就會同步阻塞直至任務完成(由于臨時對象的析構函數)。所以,即使不關心返回值,也總要用auto來配合async(),避免同步阻塞

5)、小結
  1. 多線程是并發最常用的實現方式,好處是任務并行、避免阻塞,壞處是開發難度高,有數據競爭、死鎖等很多坑
  2. call_once()實現了僅調用一次的功能,避免多線程初始化時的沖突
  3. thread_local實現了線程局部存儲,讓每個線程都獨立訪問數據,互不干擾
  4. atomic實現了原子化變量,可以用作線程安全的計數器,也可以實現無鎖數據結構
  5. async()啟動一個異步任務,相當于開了一個線程,但內部通常會有優化,比直接使用線程更好

參考:

12 | 三分天下的容器:恰當選擇,事半功倍

13 | 五花八門的算法:不要再手寫for循環了

14 | 十面埋伏的并發:多線程真的很難嗎?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/41743.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/41743.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/41743.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RAG 工業落地方案框架(Qanything、RAGFlow、FastGPT、智譜RAG)細節比對!CVPR自動駕駛最in挑戰賽賽道,全球冠軍被算力選手奪走了

RAG 工業落地方案框架&#xff08;Qanything、RAGFlow、FastGPT、智譜RAG&#xff09;細節比對&#xff01;CVPR自動駕駛最in挑戰賽賽道&#xff0c;全球冠軍被算力選手奪走了。 本文詳細比較了四種 RAG 工業落地方案 ——Qanything、RAGFlow、FastGPT 和智譜 RAG&#xff0c;重…

git push之后回滾到某個版本

背景 因為粗心在主分支上修改了代碼&#xff0c;push了上去&#xff0c;污染了主分支&#xff0c;希望將主分支之后的修改回滾&#xff0c;包括提交記錄&#xff0c;就是遠程的記錄中回到希望回到的版本&#xff0c;保持干凈。 git push -f 可以做到&#xff0c;會沖掉所有的…

SwiftUI 6.0(iOS 18.0)滾動視圖新增的滾動階段(Scroll Phase)監聽功能趣談

何曾幾時&#xff0c;在 SwiftUI 開發中的禿頭小碼農們迫切需要一種能夠讀取當前滾動狀態的方法。 在過去&#xff0c;他們往往需要借助于 UIKit 的神秘力量。不過這一切在 SwiftUI 6.0 中已成“滄海桑田”。 在本篇博文中&#xff0c;您將學到如下內容&#xff1a; 1. Scroll…

一份適合新手的軟件測試練習項目

最近&#xff0c;不少讀者托我找一個能實際練手的測試項目。開始&#xff0c;我覺得這是很簡單的一件事&#xff0c;但當我付諸行動時&#xff0c;卻發現&#xff0c;要找到一個對新手友好的練手項目&#xff0c;著實困難。 我翻了不下一百個web網頁&#xff0c;包括之前推薦練…

nginx的知識面試易考點

Nginx概念 Nginx 是一個高性能的 HTTP 和反向代理服務。其特點是占有內存少&#xff0c;并發能力強&#xff0c;事實上nginx的并發能力在同類型的網頁服務器中表現較好。 Nginx 專為性能優化而開發&#xff0c;性能是其最重要的考量指標&#xff0c;實現上非常注重效率&#…

C#用鏈表和數組分別實現堆棧

1.鏈表 實現棧的四個基本功能 入棧 出棧 長度 棧頂值 public class 基礎 : MonoBehaviour {public class MyStack{//定義每一個元素的數據結構 //下一個元素 和 該元素的值public class StackData{public StackData next;public object data;public StackData(StackData next,…

linux驅動編程 - kfifo先進先出隊列

簡介&#xff1a; kfifo是Linux Kernel里面的一個 FIFO&#xff08;先進先出&#xff09;數據結構&#xff0c;它采用環形循環隊列的數據結構來實現&#xff0c;提供一個無邊界的字節流服務&#xff0c;并且使用并行無鎖編程技術&#xff0c;即當它用于只有一個入隊線程和一個出…

nginx修改網站默認根目錄及發布(linux、centos、ubuntu)openEuler軟件源repo站點

目錄 安裝nginx配置nginx其它權限配置 安裝nginx dnf install -y nginx配置nginx whereis nginxcd /etc/nginx llcd conf.d touch vhost.conf vim vhost.conf 命令模式下輸入:set nu或:set number可以顯示行號 復制如下內容&#xff1a; server {listen 80;server_name…

【0294】Postgres內核 dynahash 之 hash_search 實現原理

相關文章: 【0289】Postgres內核之哈希表(Hash Tables) 【0290】Postgres內核之dynahash(動態哈希表,dynamic hash tables)(概念篇) 【0291】Postgres內核之dynahash table 創建 【0292】Postgres內核源碼之dynahash 插入entry實現 【0293】Postgres內核之創建 dynahas…

ESP32 通過藍牙顯示歌詞代碼示例

通過藍牙協議播放音樂&#xff0c;有的時候需要顯示歌詞&#xff0c;這里就是a2dp庫獲取了歌詞 值得注意的是要想正確獲取到歌詞&#xff0c;必須打開各種播放器的字幕&#xff08;歌詞&#xff09;開關 本項目用了三個開源庫 a2dp&#xff0c;tft_espi,xfont. a2dp &#x…

基于python實現的監聽服務接口是否正常,發送異常消息到釘釘群

獲取釘釘機器人 創建釘釘群組(要求至少三個成員)進入群組 設置>機器人>添加機器人選擇自定義機器人 按照要求填寫完獲取到 Webhook的鏈接 實現代碼 from time import sleep import requests import json from datetime import datetime import logging# 配置日志記錄的…

數據結構第11節: B樹

B樹是一種自平衡的樹數據結構&#xff0c;它能夠保持數據排序&#xff0c;并且在插入、刪除和查找操作中具有對數時間復雜度。B樹廣泛應用于文件系統、數據庫和索引中&#xff0c;因為它們可以有效地處理大量數據。 B樹的特點&#xff1a; 所有葉子節點都位于同一層。每個節點…

【】AI八股-神經網絡相關

Deep-Learning-Interview-Book/docs/深度學習.md at master amusi/Deep-Learning-Interview-Book GitHub 網上相關總結&#xff1a; 小菜雞寫一寫基礎深度學習的問題&#xff08;復制大佬的&#xff0c;自己復習用&#xff09; - 知乎 (zhihu.com) CV面試問題準備持續更新貼 …

.net 調用海康SDK的跨平臺解決方案

??歡迎點贊 :?? 收藏 ?留言 ?? 如有錯誤敬請指正,賜人玫瑰,手留余香!??本文作者:由webmote 原創??作者格言:新的征程,我們面對的不僅僅是技術還有人心,人心不可測,海水不可量,唯有技術,才是深沉黑夜中的一座閃爍的燈塔序言 上2篇海康SDK使用以及常見的坑…

PCL 點云PFH特征描述子

點云PFH特征描述子 一、概述1.1 概念1.2 算法原理一、代碼實現二、結果示例一、概述 1.1 概念 點特征直方圖PFH(Point Feature Histograms)描述子:用于表示點云中每個點的局部幾何形狀信息,它是一種直方圖描述子,包括了點云的法線方向和曲率信息,PFH描述子可以幫助區分不同…

深入Django(八)

掌握Django的管理后臺 引言 在前七天的教程中&#xff0c;我們介紹了Django的基礎架構、模型、視圖、模板、URL路由、表單系統以及數據庫遷移。今天&#xff0c;我們將深入了解Django的管理后臺&#xff0c;這是一個功能強大的內置管理界面&#xff0c;用于創建、更新、查看和…

【JavaEE精煉寶庫】文件操作(1)——基本知識 | 操作文件——打開實用性編程的大門

目錄 一、文件的基本知識1.1 文件的基本概念&#xff1a;1.2 樹型結構組織和目錄&#xff1a;1.3 文件路徑&#xff08;Path&#xff09;&#xff1a;1.4 二進制文件 VS 文本文件&#xff1a;1.5 其它&#xff1a; 二、Java 操作文件2.1 方法說明&#xff1a;2.2 使用演示&…

QT面試筆記總計

一 Qt 保證多線程安全? 使互斥鎖保證多線程安全性。QMutex類、。使用讀寫鎖保證多線程安全性&#xff0c;QReadWriteLock。使用信號和槽機制保證多線程安全性。使用顯示切換保證多線程安全性。QTread類。 Qt 中的事件與信號的區別? 事件與信號的實現機制不同&#xff1b;事…

HCIA綜合實驗

學習新思想&#xff0c;爭做新青年。今天學習的是HCIA綜合實驗&#xff01; 實驗拓撲 實驗需求 總部&#xff1a; 1、除了SW8 SW9是三層交換機&#xff0c;其他交換機均為2層交換機。 2、GW為總部的出口設備&#xff0c;使用單臂路由技術&#xff0c;VLAN10,20,100的網關都在GW…

ERROR: “armeabi-v7a“ not supported for HarmonyOS

IDE 從 devecostudio-mac-4.1.3.700 升級至 devecostudio-mac-5.0.3.403 后拋出了如下異常: ERROR: "armeabi-v7a" not supported for HarmonyOS. 解決辦法 一.entry/build-profile.json5 需 entry/build-profile.json5 的 abiFilters 中移除 "armeabi-v7a&qu…