这一篇所涉及到的主要是C++语言基础,以及算法与数据结构的考点。实际上这些内容,各大公司都会有考察,无论是笔试还是面试,都离不开算法与数据结构,因为确实是技术岗的基础。 甚至有些公司,例如微软,特别看重手写代码的能力,现场给你一块小黑板,你不得不在短时间内组织好思路并书写,考虑边界、格式、性能等等。 因为SLAM领域中主要以C++语言为主(实时性要求),所以这里我们不提及其他语言了。
如果你的Linux或者C++语言或者数据结构或者算法基础不是很好,那么,赶紧再多看几遍书吧,这里有新手礼包相赠——
接下来让我们进入正题。
目录
1. 多线程的了解
2. stl有什么?
3. vector扩充方式,size与capacity区别
4. 顺序存储结构有哪些?
5. 左值引用与右值引用
6. map与unordered map区别
7. const与static、const在函数前与函数后区别
8. 虚函数与纯虚函数区别,虚函数关键字
9. 函数memcpy 、memset的实现,手撕代码
10. 一行代码求平方根
11. 各种排序时间空间复杂度(快排,归并,桶排,堆排),手撕代码
12. 最长公共子串、最长公共子序列,手撕代码
13. 树的DFS与BFS、树的遍历,手撕代码
14. 对于n个实例的k维数据,建立kd tree的时间复杂度
15. 哈夫曼树带权路径长度、哈夫曼编码
16. 长度为n的list,删除、插入与随机访问的计算复杂度
17. 在有序表中二分查找关键字所需进行的关键字比较次数是多少?
18. 字符串子串数目
19. 三维空间最近邻搜索的常用数据结构(八叉树、kd tree)
20. HashMap和Hashtable的比较
21. 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数
1. 多线程的了解
为简单起见,以下为多线程入门的一些资料,我们暂时先了解到这。
C++11多线程简介: https://www.cnblogs.com/zhangbaochong/p/5578701.html
C++多线程 | 菜鸟教程 : https://www.runoob.com/cplusplus/cpp-multithreading.html
C++11并发编程基础(一):并发、并行与C++多线程: https://www.cnblogs.com/lpxblog/p/5190438.html
C++11 多线程管理:https://www.oschina.net/translate/cplusplus-11-threading-make-your-multitasking-life?print
多线程编程(一) ——初识:https://segmentfault.com/a/1190000016171072
c++11 线程:让你的多线程任务更轻松:
https://www.oschina.net/translate/cplusplus-11-threading-make-your-multitasking-life?print
----------------------------------------------------------------------------------------------------------------------------------------
最近又看了一些并发编程的内容,对于新手内容还是不少……这里再补充一波——
C++11 Thread多线程学习心得与问题 https://www.cnblogs.com/code-wangjun/p/7476559.htm
C++11并发之std::thread https://www.cnblogs.com/lidabo/p/7852033.html
C++11中std::mutex的使用 https://blog.csdn.net/fengbingchun/article/details/73521630
基于C++11并发库的线程池与消息队列多线程框架——std::mutex类 https://blog.csdn.net/godqiao/article/details/81088860
-------------------------------------------------------------------------------------------------------------------------------------------
一文入门C++多线程编程:
C++11多线程编程基础入门 https://blog.csdn.net/krais_wk/article/details/81095899
--------------------------------------------------------------------------------------------------------------------------------------------
终极武器: C++ Reference http://www.cplusplus.com/reference/mutex/
(强烈建议如果英文没问题,你可以直接跳过以上所有的博客只看C++ Reference网站就够了,权威而且示例清楚)
多线程编程还是有点晕的,最近楼主在看ORBSLAM中的多线程操作,还没有完全弄懂。
2. stl有什么?
STL基本概念与容器——
C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。核心包括三个组件:
组件 | 描述 |
---|---|
容器(Containers) | 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。 |
算法(Algorithms) | 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。 |
迭代器(iterators) | 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。 |
C++中有两种类型的容器:顺序容器和关联容器,顺序容器主要有:vector、list、deque等。其中vector表示一段连续的内存地址,基于数组的实现,list表示非连续的内存,基于链表实现。deque(双端队列)与vector类似,但是对于首元素提供删除和插入的双向支持。关联容器主要有map和set。map是key-value形式的,set是单值。map和set只能存放唯一的key值,multimap和multiset可以存放重复key值。 vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为循环队列,set 的底层为红黑树,hash_set 的底层为哈希表。
更全面的介绍——
STL 是 C++ 通用库,基本结构由迭代器、算法、容器、仿函数、配接器和配置器(即内存配置器)组成。目前,STL 中已经提供的容器主要如下:
STL简介: http://c.biancheng.net/view/1436.html
STL查询详细大全: http://c.biancheng.net/stl/
3. vector扩充方式,size与capacity区别
C++ STL 之 vector 的 capacity 和 size 属性区别——
size 是当前 vector 容器真实占用的大小,也就是容器当前拥有多少个容器。
capacity 是指在发生 realloc 前能允许的最大元素数,即预分配的内存空间。
当然,这两个属性分别对应两个方法:resize() 和 reserve()。
使用 resize() 容器内的对象内存空间是真正存在的。
使用 reserve() 仅仅只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(空间是"野"的)。
此时切记使用 [] 操作符访问容器内的对象,很可能出现数组越界的问题。
下面用例子进行说明:
#include
#include
using std::vector;
int main(void)
{
vector v;
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
v.reserve(10);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
v.resize(10);
v.push_back(0);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
return 0;
}
运行结果为:(win 10 + VS2010)
注: 对于 reserve(10) 后接着直接使用 [] 访问越界报错(内存是野的),大家可以加一行代码试一下,我这里没有贴出来。
这里直接用[]访问,vector 退化为数组,不会进行越界的判断。此时推荐使用 at(),会先进行越界检查。
相关引申——
针对 capacity 这个属性,STL 中的其他容器,如 list map set deque,由于这些容器的内存是散列分布的,因此不会发生类似 realloc() 的调用情况,因此我们可以认为 capacity 属性针对这些容器是没有意义的,因此设计时这些容器没有该属性。在 STL 中,拥有 capacity 属性的容器只有 vector 和 string。
另,上述介绍中提到了realloc,我们不妨再学习一下:
void *realloc (void *ptr, size_t new_size )
是C中的函数,相关的还有malloc和free函数。在C++中主要用new和delete了。 realloc可能会改变指针的地址,不建议使用。 realloc函数用于修改一个原先已经分配的内存块的大小,可以使一块内存的扩大或缩小。若nuw_size > size,即扩大*ptr所指向的内存空间,如果原先的内存尾部有足够的扩大空间,则直接在原先的内存块尾部新增内存,如果原先的内存尾部空间不足,或原先的内存块无法改变大小,realloc将重新分配另一块nuw_size大小的内存,并把原先那块内存的内容复制到新的内存块上。
4. 顺序存储结构有哪些?
存储结构分四类:顺序存储、链接存储、索引存储 和 散列存储。https://blog.csdn.net/wengchen123/article/details/78373000
C++中几种常见的顺序存储结构(https://blog.csdn.net/wonitawonitawonita/article/details/79924068)——
5. 左值引用与右值引用
左值是表达式结束后依然存在的持久对象;右值是表达式结束时就不再存在的临时对象。根本判断是用取地址符&,左值可以,右值不行。
左值引用(lvalue reference):可以绑定到左值的引用。右值引用(rvalue reference):指向一个将要销毁的对象的引用。 引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义;对引用的一切操作,就相当于对原对象的操作。
左值引用的基本语法:type &引用名 = 左值表达式;
右值引用的基本语法type &&引用名 = 右值表达式;
右值引用主要就是解决一个拷贝效率低下的问题,因为针对于右值,或者打算更改的左值,我们可以采用类似与unique_ptr的move(移动)操作,大大的提高性能(move semantics)。
右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。详细:C++11 标准新特性: 右值引用与转移语义(https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/),它的主要目的有两个方面:
对于左值引用和右值引用的使用举个例子——
#include
#include
#include
namespace lvalue_rvalue_ {
namespace {
char& get_val(std::string& str, std::string::size_type ix)
{
return str[ix];
}
}
int test_lvalue_rvalue_1()
{
{
// 赋值运算符的左侧运算对象必须是一个可修改的左值:
int i = 0, j = 0, k = 0; // 初始化而非赋值
const int ci = i; // 初始化而非赋值
// 下面的赋值语句都是非法的
//1024 = k; // 错误:字面值是右值
//i + j = k; // 错误:算术表达式是右值
//ci = k; // 错误:ci是常量(不可修改的)左值
}
{
int i = 0;
int& r = i; // 正确:r引用i
//int&& rr = i; // 错误:不能将一个右值引用绑定到一个左值上
//int& r2 = i * 42; // 错误:i*42是一个右值
const int& r3 = i * 42; // 正确:我们可以将一个const的引用绑定到一个右值上
int&& rr2 = i * 42; // 正确:将rr2绑定到乘法结果上
}
{
// 不能将一个右值引用绑定到一个右值引用类型的变量上
int&& rr1 = 42; // 正确:字面常量是右值
//int&& rr2 = rr1; // 错误:表达式rr1是左值
//int rr = &&rr1; // 不能将一个右值引用直接绑定到一个左值上
int i = 5;
int&& rr3 = std::move(i); // C++11,std::move函数是将左值转换为对应的右值引用类型,这一步OK!!!
}
// 调用一个返回引用的函数得到左值,其它返回类型得到右值
std::string s{ "a value" };
std::cout << s << std::endl; // a value
get_val(s, 0) = 'A';
std::cout << s << std::endl; // A value
return 0;
6. map与unordered map区别
都属于关联容器,存储元素的类型都为Key-Value。也就是pair
在C++11中有新出4个关联式容器:unordered_map/unordered_set/unordered_multimap/unordered_multiset。
map——
unordered_map——
因此如果在不涉及顺序并且能够设计出较好的hash算法(碰撞较少)的情况下,unordered_map是最好的选择。但是如果考虑顺序的因素,仍旧需要选择map。设计动态数据结构的时候(动态插入与删除),红黑树是较好的选择。
7. const与static、const在函数前与函数后区别
8. 虚函数与纯虚函数区别,虚函数关键字
C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
纯虚函数的声明: virtual 返回类型 函数名(参数表)= 0
在父类中没有定义。无法定义抽象类的对象,可以定义指向抽象类的指针
抽象类的作用是保证继承层次的每个类都具有纯虚函数所要求的行为,保证了子类都必须定义该虚函数。
9. 函数memcpy 、memset的实现,手撕代码
memcpy——
头文件:#include
函数原型:void *memcpy(void *dest, const void *src, size_t n)
功能:将指针src指向的内存空间的n个字节复制到dest指针指向的内存空间
参数:src 为原内容内存的起始地址,dest为复制到目标地址的起始地址
返回值:目标dest内存的起始地址
注意:1、内存空间不能够有重叠; 2、memcpy对于需要复制的内容没有限制,因此用途更广; 3、很明确的是memcpy是将 n个字节, 虽然memcpy对复制的内容完全没有任何的限制,比如数组,结构体等特殊的结构,如果你想将整个结构体变量的内容复制到dest内存区,最好使用sizeof将要复制的内容的完整大小求出来赋值给n,以保持复制的完整性;
( 原文:https://blog.csdn.net/u011118276/article/details/46742341/ )
void *memCpy(void *dest, const void *src, size_t n)
{
if (NULL == dest || NULL == src || n < 0)
return NULL;
char *tempDest = (char *)dest;
char *tempSrc = (char *)src;
while (n-- > 0)
*tempDest++ = *tempSrc++;
return dest;
}
//考虑内存重叠的情况
void* memcpy2(void* desc, const void * src, size_t size)
{
if(desc == NULL && src == NULL)
{
return NULL;
}
unsigned char* desc1 = (unsigned char*)desc;
unsigned char* src1 = (unsigned char*)src;
//当内存重叠时,从后往前复制
if(desc > src && desc1 < (src1 + size))//内存发生重叠
{
for (int i = size - 1; i >= 0; i--)
{
*desc1++ = *src1++;
}
}
else
{
for (int i = 0; i < size; i++)
{
*desc1++ = *src1++;
}
}
return desc;
}
memset——
头文件:#include
函数原型:void *memset(void *s, int c, size_t n)
功能:以s为起始位置的n个字节的内存区域用整数c进行填充
参数:s为内存区域的起始位置,c为要填充的字符,n为要填充多少个字节
返回值:目标s内存的起始地址
注意: 1、n表示的是字节数,函数是以字节的形式每次赋值给目标地址;2、memset函数也是以字节为单位进行赋值的,所以要想在整形数组中给每一位赋确定的非0值,一般来讲是不可行的;(下方将有对此说明测试的程序)
void *memSet(void *s, int c, size_t n)
{
if (NULL == s || n < 0)
return NULL;
char * tmpS = (char *)s;
while(n-- > 0)
*tmpS++ = c;
return s;
}
10. 一行代码求平方根
参考https://blog.csdn.net/u013146882/article/details/72350873
三种方法分别是二分法、牛顿法和卡马克算法(本质上也是牛顿法,初始值取得很巧妙)。代码如下:
#include
using namespace std;
double sqrt_math1(double x) { //二分法,不断取中间值进行迭代
if (x < 0) return -1;
double low = (x >= 1 ? 1 : x), high = (x >= 1 ? x : 1), mid = (low + high) / 2;
while (abs(mid*mid - x) > 0.000001) {
if (mid*mid > x) high = mid;
else low = mid;
mid = (low + high) / 2;
}
return mid;
}
double sqrt_math2(double x) { //牛顿法,速度比二分法快
if (x < 0) return -1;
double pre = x, tem = (pre + x / pre) / 2;
while (abs(tem*tem - x) > 0.000001) {
tem = (tem + x / tem) / 2;
}
return tem;
}
float sqrt_math3(float x) { //卡马克快速平方根算法(游戏编程经典算法),比牛顿法快
if (x < 0) return -1;
float xhalf = 0.5f*x;
int i = *(int*)&x; // get bits for floating VALUE
i = 0x5f375a86 - (i >> 1); // gives initial guess y0
x = *(float*)&i; // convert bits BACK to float
x = x*(1.5f - xhalf*x*x); // Newton step, repeating increases accuracy
x = x*(1.5f - xhalf*x*x); // Newton step, repeating increases accuracy
x = x*(1.5f - xhalf*x*x); // Newton step, repeating increases accuracy
return 1 / x;
}
int main() {
double x;
cout << "Input the number:";
while (cin >> x) {
cout << sqrt_math1(x) << endl;
cout << sqrt_math2(x) << endl;
cout << sqrt_math3(x) << endl;
}
system("pause");
return 0;
}
但是题目中说一行代码求平方根 。。。
改用Python:
11. 各种排序时间空间复杂度(快排,归并,桶排,堆排),手撕代码
我的口诀: 不稳定排序的有:快速直接堆希尔。
我们以快速排序为例(来自剑指offer)——
实现快速排序的核心函数是一个分割函数(Partition),在数组中任意选择一个数index,将数组中的数字分为两部分,比index小的移到左边,大的移到右边。 这个函数的实现(不是一下就能理解,多看几遍):
int Partition(int data[], int length, int start, int end) //for example start=0 end=length-1
{
//判断合法输入
if(data==nullptr || length<=0 || start<=0 || end<=0 )
throw new exception("Invalid Parameters.");
int index = RandomInRange(start, end);
int small = start - 1; //代表比标识小的数
Swap(&data[index], &data[end]); //把标识放在end
for(int i=start; i
Patition这个函数不仅仅是在快速排序中可以用到,有时一些算法题中也可以使用。 接下来,我们看一下快排的主函数:
void QuikSort(int data[], int length, int start, int end) //因为函数中要用到递归 后面的两个形参不能少
{
if(data == nullptr || length <= 0)
throw new std::exception("valid parameters.");
if(satrt == end)
return;
int index = Partition(data, length, start, end);
if(start < index) //start==index 时 前半部分不用排序
QuikSort(data, length, start, index-1);
if(end > index)
QuikSort(data, length, index+1, end);
return;
}
OK,关于其他的排序方法这里就不介绍了,如有机会,后面我们再写一篇博客,详细阐述这些排序方法。
12. 最长公共子串、最长公共子序列,手撕代码
最长公共子串(Longest Common Substring),子串是连续的;
最长公共子序列(Longest Common Subsequences), 不一定是连续的
最长公共子序列——
参考我之前写得一篇博客:https://blog.csdn.net/weixin_43795395/article/details/89554031
“删除最少的字符串使得剩下成为回文串” :我想掌握了这道题,基本上最长公共子序列方法和代码应该没问题。 这里就简要说一下函数关系式: 不妨设函数参数为m, n,分别表示str1和str2字符串的下标位置。 于是,求函数表达式 L(m, n)!
最长公共子串——
跟上面有点类似,这里我们直接分析函数关系式 (注意起始状态和上面的不同,而且i = 0 或者j = 0应该指示的是字符串中的下标减1)
#include
#include
using namespace std;
//dp[i][j]:串(x1,x2,...,xi)与串(y1,y2,...,yj),
//d[i][j]表示这两个串结与最长公共子串结尾相同时,最长公共子串的长度
//状态转移方程如下:
//若i=0或j=0,则dp[i][j] = 0
//否则:
// 若A[i]==B[j],则dp[i][j] = dp[i-1][j-1] + 1
// 若A[i]!=B[j],则dp[i][j] = 0
//用于打印的函数,后面才用到
void print_substring(string str, int end, int length)
{
int start = end - length + 1;
for(int k=start;k<=end;k++)
cout << str[k];
cout << endl;
}
int main()
{
string A,B;
cin >> A >> B;
int x = A.length();
int y = B.length();
A = " " + A;//特殊处理一下,便于编程
B = " " + B;
//回忆一下dp[][]的含义?
int **dp = new int* [x+1];
int i,j;
for(i=0;i<=x;i++)
{
dp[i] = new int[y+1];
for(j=0;j<=y;j++)
dp[i][j] = 0;
}
//下面计算dp[i][j]的值并记录最大值
int max_length = 0;
for(i=1;i<=x;i++)
for(j=1;j<=y;j++)
if(A[i]==B[j])
{
dp[i][j] = dp[i-1][j-1] + 1;
if(dp[i][j]>max_length)
max_length = dp[i][j];
}
else
dp[i][j] = 0;
//LCS的长度已经知道了,下面是根据这个最大长度和dp[][]的值,
//找到对应的 LCS具体子串, 注意:可能有多个
int const arr_length = (x>y?x:y) + 1;
int end_A[arr_length]; //记录LCS在字符串A中结束的位置
int num_max_length = 0; //记录LCS的个数
for(i=1;i<=x;i++)
for(j=1;j<=y;j++)
if(dp[i][j] == max_length)
end_A[num_max_length++] = i;
cout << "the length of LCS(substring) is : " << max_length << endl << " nums: " << num_max_length << endl << "they are (it is): " << endl;
for(int k=0;k
13. 树的DFS与BFS、树的遍历,手撕代码
BFS和DFS的原理以及代码实践,我在之前的一篇博客中做了较详细的讨论,请见https://blog.csdn.net/weixin_43795395/article/details/89399874。
这里我们主要来看对树用这两种搜索方法。 (参考https://blog.csdn.net/m0_37973607/article/details/86290964)
深度优先搜索(Depth First Search)——
是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。以下面二叉树为例,深度优先搜索的顺序为:ABDECFG。DFS是先访问根结点,然后遍历左子树接着是遍历右子树,因此我们可以利用堆栈的先进后出的特点,现将右子树压栈,再将左子树压栈,这样左子树就位于栈顶,可以保证结点的左子树先与右子树被遍历。
广度优先搜索(Breadth First Search)——
又叫宽度优先搜索或横向优先搜索,是从根结点开始沿着树的宽度搜索遍历,上面二叉树的遍历顺序为:ABCDEFG。可以利用队列实现广度优先搜索。
1 #include
2 #include
3 #include
4 #include
5 using namespace std;
6
7 struct BitNode
8 {
9 int data;
10 BitNode *left, *right;
11 BitNode(int x) :data(x), left(0), right(0){}
12 };
13
14 void Create(BitNode *&root)
15 {
16 int key;
17 cin >> key;
18 if (key == -1)
19 root = NULL;
20 else
21 {
22 root = new BitNode(key);
23 Create(root->left);
24 Create(root->right);
25 }
26 }
27
28 void PreOrderTraversal(BitNode *root)
29 {
30 if (root)
31 {
32 cout << root->data << " ";
33 PreOrderTraversal(root->left);
34 PreOrderTraversal(root->right);
35 }
36 }
37
38 //深度优先搜索
39 //利用栈,现将右子树压栈再将左子树压栈
40 void DepthFirstSearch(BitNode *root)
41 {
42 stack nodeStack;
43 nodeStack.push(root);
44 while (!nodeStack.empty())
45 {
46 BitNode *node = nodeStack.top();
47 cout << node->data << ' ';
48 nodeStack.pop();
49 if (node->right)
50 {
51 nodeStack.push(node->right);
52 }
53 if (node->left)
54 {
55 nodeStack.push(node->left);
56 }
57 }
58 }
59
60 //广度优先搜索
61 void BreadthFirstSearch(BitNode *root)
62 {
63 queue nodeQueue;
64 nodeQueue.push(root);
65 while (!nodeQueue.empty())
66 {
67 BitNode *node = nodeQueue.front();
68 cout << node->data << ' ';
69 nodeQueue.pop();
70 if (node->left)
71 {
72 nodeQueue.push(node->left);
73 }
74 if (node->right)
75 {
76 nodeQueue.push(node->right);
77 }
78 }
79 }
80
81 int main()
82 {
83 BitNode *root = NULL;
84 Create(root);
85 //前序遍历
86 PreOrderTraversal(root);
87 //深度优先遍历
88 cout << endl << "dfs" << endl;
89 DepthFirstSearch(root);
90 //广度优先搜索
91 cout << endl << "bfs" << endl;
92 BreadthFirstSearch(root);
93 }
14. 对于n个实例的k维数据,建立kd tree的时间复杂度
关于Kd树,这里有一篇非常详细的文章,从K近邻算法谈到Kd树,以及Kd树的算法实现。
参考 https://blog.csdn.net/likika2012/article/details/39619687
kd树构建的伪代码:
再举一个简单直观的实例来介绍k-d树构建算法。假设有6个二维数据点{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},数据点位于二维空间内,如下图所示。为了能有效的找到最近邻,k-d树采用分而治之的思想,即将整个空间划分为几个小部分,首先,粗黑线将空间一分为二,然后在两个子空间中,细黑直线又将整个空间划分为四部分,最后虚黑直线将这四部分进一步划分。
6个二维数据点{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}构建kd树的具体步骤为:
如上算法所述,kd树的构建是一个递归过程,我们对左子空间和右子空间内的数据重复根节点的过程就可以得到一级子节点(5,4)和(9,6),同时将空间和数据集进一步细分,如此往复直到空间中只包含一个数据点。
与此同时,经过对上面所示的空间划分之后,我们可以看出,点(7,2)可以为根结点,从根结点出发的两条红粗斜线指向的(5,4)和(9,6)则为根结点的左右子结点,而(2,3),(4,7)则为(5,4)的左右孩子(通过两条细红斜线相连),最后,(8,1)为(9,6)的左孩子(通过细红斜线相连)。如此,便形成了下面这样一棵k-d树:
也就是说,如之前所述,kd树中,kd代表k-dimension,每个节点即为一个k维的点。每个非叶节点可以想象为一个分割超平面,用垂直于坐标轴的超平面将空间分为两个部分,这样递归的从根节点不停的划分,直到没有实例为止。经典的构造k-d tree的规则如下:
对于n个实例的k维数据来说,建立kd-tree的时间复杂度为O(k*n*logn)。
(构建堆得时间复杂度为O(n), 构建二叉查找树的时间复杂度为 n*log2n )
15. 哈夫曼树带权路径长度、哈夫曼编码
哈夫曼树的构造——
哈夫曼树在构造时每次从备选节点中挑出两个权值最小的节点进行构造,每次构造完成后会生成新的节点,将构造的节点从备选节点中删除并将新产生的节点加入到备选节点中。新产生的节点权值为参与构造的两个节点权值之和。举例如下:
带权路径长度计算——
(a) WPL = 7x2+5x2+2x2+4x2=36 (b) WPL = 2X1+4X2+7X3+5X3 = 46 (c) WPL = 7x1+5x2+2x3+4x3 = 35
即所有叶节点的带权路径之和为该树的带权路径长度(WPL)。 某个叶节点的带权路径等于该叶节点的深度(根的深度为0)乘以权重,也就是从根到叶节点所走过边的数量。
哈夫曼树的应用——
在处理字符串序列时,如果对每个字符串采用相同的二进制位来表示,则称这种编码方式为定长编码。若允许对不同的字符采用不等长的二进制位进行表示,那么这种方式称为可变长编码。可变长编码其特点是对使用频率高的字符采用短编码,而对使用频率低的字符则采用长编码的方式。这样我们就可以减少数据的存储空间,从而起到压缩数据的效果。而通过哈夫曼树形成的哈夫曼编码是一种的有效的数据压缩编码。
如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。如0,101和100是前缀编码。由前缀码形成的序列可以被唯一的组成一个字符串序列。如00101100可以被唯一的分析为0,0,101和100。
示例: 我们对一个字符串进行统计发现a-f出现的频率分别为a:45,b:13,c:12,d:16,e:9,f:5,我们对该字符串进行采用哈夫曼编码进行存储。
WPL = 1x45+3x(13+12+16)+4x(5+9)=224
这样算下来使用224二进制位就可以将该字符串存储起来,因为哈夫曼码是前缀码,所以可以唯一的还原出原来的字符序列。如果我们每个字符使用3位进行存储(至少3位),那么需要300bit才能将该字符串存储下。
16. 长度为n的list,删除、插入与随机访问的计算复杂度
插入、删除数据效率高,时间复杂度为O(1),只需更改指针指向即可。
随机访问效率低,时间复杂度为O(n),需要从链头至链尾进行遍历。
和数组相比,链表的内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。
常见的链表结构有:
数组与链表的对比——
数组的缺点
链表的缺点
如何选择链表和数组
17. 在有序表中二分查找关键字所需进行的关键字比较次数是多少?
举个例子 { 12, 24, 36, 48, 60, 72, 84 } 查找72需要比较的次数 ?
共7个数,下标范围 0 ~ 6. 查找之后的结果有三种,要么找到了,要么low = mid + 1, 要么 high = mid - 1
二分查找开始:low = 0, high = 6;
第一次查找: mid = low + high / 2 = 3, 数字为48, 不等与72; low = mid + 1 = 4;
第二次查找: mid = low + high / 2 = 4 + 6 / 2 = 5, 数字为72, 成功。 所以一共比较了两次。
最多比较 log2(N) 向下取整
18. 字符串子串数目
想象一个字符串string,子串长度为1的共有n, 子串长度为2的共有n - 1,我就不画图了很容易想象,一直到子串长度为n的有1个。 所有总数 = n + (n - 1) + (n - 2) + ... + 1 = n*(n - 1) / 2 。
最后空串也算一个,所以子串数目为 n * (n - 1) / 2 + 1 .
19. 三维空间最近邻搜索的常用数据结构(八叉树、kd tree)
参考《slam十四讲》——
20. HashMap和Hashtable的比较
这两个数据结构应该主要实在Jave中讲的。。。
https://blog.csdn.net/qq_35181209/article/details/74503362
C++中的hashmap跟unordered_map差不多。
C++中map、hash_map、unordered_map、unordered_set的区别: https://blog.csdn.net/chen134225/article/details/83106569
21. 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数
这
个就有点简单了,但是主要代码要写的完整准确。 这是来自剑指offer上的一道题,
二维数组中数的查找
题目:二维数组中从左到右增序 从上到下增序 实现一个函数输入为一个二维数组和整型数 判断二维数组中是否存在该整数
bool Find(int* matrix, int rows, int cols, int number)
{
bool found = false;
if(matrix != nullptr rows > 0 && cols > 0) //判断输入的合法性
{
int row = 0;
int col = cols -1 ;
while(row < rows && col >= 0) //循环结束的条件是行或者列超标了
{
int tmp = matrix[row*cols + col]; //第row行col列的元素 这里每次循环还要创建局部变量 其实可以不写 直接在括号中做比较
if(tmp == number)
found = true;
break; //若存在及时跳出while循环 减小时间
//如果不是只有两种可能 比其大 或者比其小
else if(tmp > number)
--col;
else
++row;
}
}
return found;
}
写博不易,您的支持让知识之花绽放得更美丽 ~_~