找工作面试会遇到手撕代码的情况,这里学习一下刷题常用数据结构的基础,并记录一些模板,计时一周时间,完成基础的学习实践与博客记录。主要通过代码随想录来进行学习。
链表
链表分为单链表与双链表,前者一个数据域+一个指针域,后者一个数据域+两个指针域。
1、单链表定义
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
//初始化
ListNode* head = new ListNode(5);
2、链表的主要操作
删除节点,要判断好不能够空指针再进行指针。除了对前后两个节点进行链接外,要记得delete要删除的节点释放空间。对于单向链表,头节点和中间节点方法不一样,或者分开处理或者添加虚拟头节点将其他全部按照中间节点处理。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/删除指定数值的元素
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* node = head;
while(node != nullptr){
while(node->next != nullptr && node->next->val == val){
node->next = node->next->next;
}
node = node->next;
}
while(head != nullptr && head->val == val){
head = head->next;
node = head;
}
return head;
}
};
添加节点
直接改变链表指针就行。
ListNode* head;
ListNode* newNode{0};
newNode->next = head->next;
head->next = newNode;
哈希表
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希函数,又称散列算法,是一种从任何一种数据中创建数字“指纹” 的方法。哈希函数是将输入映射到固定大小的哈希值的函数。
哈希碰撞是指不同的输入数据产生了相同的哈希值。碰撞指的是两个不同的输入映射到了相同的哈希值。一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法:将冲突元素存至链表。
线性探测法:使用线性探测法,一定要保证tableSize大于dataSize。向下找一个空位。
常见三种哈希结构:数组,set,map。
红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
set和map的区别在于set只含有key,而map有一个key和key所对应的value两个元素。
#include <set>
#include <unordered_set>
#include <map>
#include <unordered_map>
using namespace std;
//set使用方法*****************************************************************************
set<int> a; // 定义一个int类型的集合a
// set<int> a(10); // error,未定义这种构造函数
// set<int> a(10, 1); // error,未定义这种构造函数
set<int> b(a); // 定义并用集合a初始化集合b
set<int> b(a.begin(), a.end()); // 将集合a中的所有元素作为集合b的初始值
int n[] = { 1, 2, 3, 4, 5 };
set<int> a(n, n + 5); // 将数组n的前5个元素作为集合a的初值
set<int> st;
set<int> st_;
st.size();//容器大小
st.max_size();//容器最大容量
st.empty();//容器判空
st.count(key);//查找键 key 的元素个数
st.insert(const T& x);//在容器中插入元素
st.insert(iterator it, const T& x);//任意位置插入一个元素
st.pop_back(const T& elem);//删除容器中值为 elem 的元素
st.erase(iterator it);//删除it迭代器所指的元素
st.erase(iterator first, iterator last);//删除区间 [first,last] 之间的所有元素
st.clear();//清空所有元素
set<int>::iterator it;
st.find(key);//查找键 key 是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end():
swap(st, st_);st.swap(st_); //交换两个同类型容器的元素:
//迭代器
st.begin();//开始迭代器指针
st.end(); // 末尾迭代器指针:指向最后一个元素的下一个位置
st.cbegin(); // 指向常量的开始迭代器指针:意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
lst.cend(); //指向常量的末尾迭代器指针
st.rbegin(); //反向迭代器指针,指向最后一个元素
st.rend(); //反向迭代器指针,指向第一个元素的前一个元素
st.lower_bound(keyElem); //返回最后一个 key<=keyElem 元素的迭代器
st.upper_bound(keyElem); //返回第一个 key>keyElem 元素的迭代器
st.equal_range(keyElem); //返回容器中 key 与 keyElem 相等的上下限的两个迭代器,这两个迭代器被放在对组(pair)中
//map使用方法**************************************************************************
map<int, string> a; // 定义一个int类型的映射a
// map<int, string> a(10); // error,未定义这种构造函数
// map<int, string> a(10, 1); // error,未定义这种构造函数
map<int, string> b(a); // 定义并用映射a初始化映射b
// map<int, string> b(a.begin(), a.end()); // error,未定义这种构造函数
map<int,string> mp;
mp.size();//容器大小
mp.max_size();//容器最大容量
mp.empty();//容器判空
mp.count(key);//查找键 key 的元素个数
mp.insert(const T& x);//在容器中插入元素
mp.insert(iterator it, const T& x);//任意位置插入一个元素
mp.pop_back(const T& keyValue);//删除键值为 keyValue 的元素
mp.erase(iterator it);//删除迭代器所指的元素
mp.erase(iterator first, iterator last);//删除区间[first,last]之间的所有元素
mp.clear();//清空所有元素
mp.find(key);//查找键 key 是否存在,若存在,返回该键的元素的迭代器;若不存在,返回 map.end():
swap(a, b);a.swap(b);交//换两个同类型容器的元素:
//迭代器
mp.begin();//开始迭代器指针:
mp.end(); // 末尾迭代器指针:指向最后一个元素的下一个位置
mp.cbegin(); // 指向常量的开始迭代器指针:意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
mp.cend(); //指向常量的末尾迭代器指针
mp.rbegin();//反向迭代器指针,指向最后一个元素
mp.rend(); //反向迭代器指针,指向第一个元素的前一个元素
mp.lower_bound(keyElem); //返回最后一个 key<=keyElem 元素的迭代器
mp.upper_bound(keyElem); //返回第一个 key>keyElem 元素的迭代器
mp.equal_range(keyElem); //返回容器中 key 与 keyElem 相等的上下限的两个迭代器,这两个迭代器被放在对组(pair)中
/*其余方法是相似的(但是个别函数仍然使用方法不一样),特性不一样:
set:只有键值,有序,不允许重复
multiset:只有键值,有序,允许重复
unordered_set:只有键值,无序,不允许重复
unordered_multiset:只有键值,无序,允许重复
map:键值+数据,有序,键值不允许重复
multimap:键值+数据,有序,键值允许重复
unordered_map:键值+数据,无序,键值不允许重复
unordered_multimap:键值+数据,无序,键值允许重复
*/
set与map用法总结博客:
https://www.cnblogs.com/linuxAndMcu/p/10261014.html
https://www.cnblogs.com/linuxAndMcu/p/10261263.html
栈和队列
队列是先进先出,栈是先进后出。
#include<stack>
stack<int> s;//参数也是数据类型,这是栈的定义方式
s.empty()//如果栈为空返回true,否则返回false
s.size()//返回栈中元素的个数
s.pop()//删除栈顶元素但不返回其值
s.top()//返回栈顶的元素,但不删除该元素
s.push(X)//在栈顶压入新元素 ,参数X为要压入的元素
#include<queue>
queue<int> q; //参数是数据类型,这是队列的定义方式
q.empty()// 如果队列为空返回true,否则返回false
q.size() // 返回队列中元素的个数
q.pop() //删除队列首元素但不返回其值
q.front() // 返回队首元素的值,但不删除该元素
q.push(X) //在队尾压入新元素 ,X为要压入的元素
q.back() //返回队列尾元素的值,但不删除该元素
二叉树
二叉树是n(n>=0)个结点的有限集合。
1、二叉树种类
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树:二叉搜索树是一个有序树。对任意结点若存在左子树或右子树,则其左子树上所有结点的关键字均小于该结点,右子树上所有结点的关键字均大于该结点。特点:可为空树,左子树所有节点<根节点<右子树,不存在值相等的节点
平衡二叉树:是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
2、二叉树存储方式
二叉树可以链式存储,也可以顺序存储。
链式存储:
数组存储:如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
3、二叉树遍历方式
- 深度优先遍历
- 前序遍历(递归法,迭代法):中左右
- 中序遍历(递归法,迭代法):左中右
- 后序遍历(递归法,迭代法):左右中
- 广度优先遍历
- 层次遍历(迭代法)
4、二叉树定义使用
//二叉树节点定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
//递归遍历
class Solution {
public:
//前序
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
//中序
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
//后序
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
//迭代遍历
//前序
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return result;
}
};
//中序
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
//后序
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if (node->right) st.push(node->right); // 空节点不入栈
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
//层次遍历
//迭代
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
//递归
class Solution {
public:
void order(TreeNode* cur, vector<vector<int>>& result, int depth)
{
if (cur == nullptr) return;
if (result.size() == depth) result.push_back(vector<int>());
result[depth].push_back(cur->val);
order(cur->left, result, depth + 1);
order(cur->right, result, depth + 1);
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
int depth = 0;
order(root, result, depth);
return result;
}
};
C++ STL容器补充
参考:C++ STL中容器的使用全面总结_stl 有序容器-CSDN博客
1、vector
vector(向量): 是一种序列式容器,事实上和数组差不多,但它比数组更优越。一般来说数组不能动态拓展,因此在程序运行的时候不是浪费内存,就是造成越界。而 vector 正好弥补了这个缺陷,当内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。
#include <vector>
vector<int> vec1; //默认初始化,vec1为空
vector<int> vec2(vec1); //使用vec1初始化vec2
vector<int> vec3(vec1.begin(),vec1.end());//使用vec1初始化vec2
vector<int> vec4(10); //10个值为0的元素
vector<int> vec5(10,4); //10个值为4的元素
vector<string> vec6(10,"null"); //10个值为null的元素
vector<string> vec7(10,"hello"); //10个值为hello的元素
vec1.push_back(100); //添加元素
int size = vec1.size(); //元素个数
bool isEmpty = vec1.empty(); //判断是否为空
cout<<vec1[0]<<endl; //取得第一个元素
vec1.insert(vec1.end(),5,3); //从vec1.back位置插入5个值为3的元素
vec1.pop_back(); //删除末尾元素
vec1.erase(vec1.begin(),vec1.end());//删除之间的元素,其他元素前移
cout<<(vec1==vec2)?true:false; //判断是否相等==、!=、>=、<=...
vector<int>::iterator iter = vec1.begin(); //获取迭代器首地址
vector<int>::const_iterator c_iter = vec1.begin(); //获取const类型迭代器
vec1.clear(); //清空元素
// 下标法(vector的特有访问方法,一般容器只能通过迭代器访问)
int length = vec1.size();
for ( int i=0;i<length;i++)
{
cout<<vec1[i];
}
cout<<endl<<endl;
// 迭代器法
vector< int >::const_iterator iterator = vec1.begin();
for (;iterator != vec1.end();iterator++)
{
cout<<*iterator;
}
2、list
List 由双向链表(doubly linked list)实现而成,元素存放在堆中,每个元素都是放在一块内存中。没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存。
#include <list>
list<int> lst1; //创建空list
list<int> lst2(3); //创建含有三个元素的list
list<int> lst3(3,2); //创建含有三个元素的值为2的list
list<int> lst4(lst2); //使用lst2初始化lst4
list<int> lst5(lst2.begin(),lst2.end()); //同lst4
lst1.assign(lst2.begin(),lst2.end()); //分配值
lst1.push_back(10); //添加值
lst1.pop_back(); //删除末尾值
lst1.begin(); //返回首值的迭代器
lst1.end(); //返回尾值的迭代器
lst1.clear(); //清空值
bool isEmpty1 = lst1.empty(); //判断为空
lst1.erase(lst1.begin(),lst1.end()); //删除元素
lst1.front(); //返回第一个元素的引用
lst1.back(); //返回最后一个元素的引用
lst1.insert(lst1.begin(),3,2); //从指定位置插入3个值为2的元素
lst1.rbegin(); //返回第一个元素的前向指针
lst1.remove(2); //相同的元素全部删除
lst1.reverse(); //反转
lst1.size(); //含有元素个数
lst1.sort(); //排序
lst1.unique(); //删除相邻重复元素
//迭代器法
for(list<int>::const_iterator iter = lst1.begin();iter != lst1.end();iter++)
{
cout<<*iter;
}
3、deque
deque(double-ended queue)是双向开口的连续内存空间(动态将多个连续空间通过指针数组接合在一起),随时可以增加一段新的空间。deque 的最大任务就是在这些分段的连续空间上,维护其整体连续的假象,并提供随机存取的接口。
#include <deque>
std::deque<int> d;
std::deque<int> d(10);
std::deque<int> d(10, 5)
std::deque<int> d1(5);
std::deque<int> d2(d1);
//拷贝普通数组,创建deque容器
int a[] = { 1,2,3,4,5 };
std::deque<int>d(a, a + 5);
//适用于所有类型的容器
std::array<int, 5>arr{ 11,12,13,14,15 };
std::deque<int> d(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
begin() //返回指向容器中第一个元素的迭代器。
end() //返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() //返回指向最后一个元素的迭代器。
rend() //返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() //和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() //和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() //和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() //和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() //返回实际元素个数。
max_size() //返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-1,我们很少会用到这个函数。
resize() //改变实际元素的个数。
empty() //判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
shrink _to_fit() //将内存减少到等于当前元素实际所使用的大小。
at() //使用经过边界检查的索引访问元素。
front() //返回第一个元素的引用。
back() //返回最后一个元素的引用。
assign() //用新元素替换原有内容。
push_back() //在序列的尾部添加一个元素。
push_front() //在序列的头部添加一个元素。
pop_back() //移除容器尾部的元素。
pop_front() //移除容器头部的元素。
insert() //在指定的位置插入一个或多个元素。
erase() //移除一个元素或一段元素。
clear() //移出所有的元素,容器大小变为 0。
swap() //交换两个容器的所有元素。
emplace() //在指定的位置直接生成一个元素。
emplace_front() //在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
emplace_back() //在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。