数据结构基础学习记录

找工作面试会遇到手撕代码的情况,这里学习一下刷题常用数据结构的基础,并记录一些模板,计时一周时间,完成基础的学习实践与博客记录。主要通过代码随想录来进行学习。

链表

链表分为单链表与双链表,前者一个数据域+一个指针域,后者一个数据域+两个指针域。

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() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。

  • 30
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独孤西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值