学习数据结构与算法的目的:
优化时间复杂度与空间复杂度 优化时间复杂度与空间复杂度 优化时间复杂度与空间复杂度
教程总纲: 暴力解法(模拟)
、算法优化(递归/二分/排序/DP)
、时刻转换(数据结构)
空间是廉价的,时间是昂贵的
相较于空间复杂度(投入金钱 增加算力),时间复杂度(消耗时间)更为重要!
降低时间与空间复杂度的方法:
例.输入数组 a = [1,2,3,4,5,5,6] 中查找出现次数最多的数值。
暴力解法是:两层for遍历,维护一个最大次数time_max,对每个元素计算出现次数time_tmp,与time_max进行对比,时间复杂度是 0 ( n 2 ) 0(n^2) 0(n2)
int main(){
vector<int> a={1,2,3,4,5,5,6};
int val_max=-1,time_max=0,time_tmp=0;
for(int i=0;i<a.size();i++){
time_tmp=0;
for(int j=0;j<a.size();j++)
if(a[i]==a[j]) time_tmp++;
if(time_tmp>time_max){
time_max=time_tmp;
val_max=a[i];
}
}
cout<<val_max<<" "<<time_max<<endl;
return 0;
}
优化思想:如何仅用单层for循环完成,用hash思想,引入k-v字典数据结构map,一次for保存每个元素出现的次数,再求每个元素次数的最大值,时间复杂度是 0 ( 2 n ) 0(2n) 0(2n)。
int main(){
vector<int> a={1,2,3,4,5,5,6};
map<int ,int> num_cnt;
int val_max,time_max=0;
for(int i=0;i<a.size();i++){
num_cnt[a[i]]++; //counting the number of times a[i occurs in the vector a.
}
for(auto it:num_cnt){ //iterating over the map and printing the max time a[i] occurs for each element.
if(time_max < it.second){
val_max=it.first; //assigning the maximum value from the map to val_max.
time_max=it.second; //assigning the maximum count from the map to time_max.
}
}
cout<<val_max<<" "<<time_max<<endl;
return 0;
}
当你不知道用什么数据结构的时候:
分析需要对数据进行了哪些操作,根据数据操作,选取合适的数据结构 分析需要对数据进行了哪些操作,根据数据操作,选取合适的数据结构 分析需要对数据进行了哪些操作,根据数据操作,选取合适的数据结构
还用上面的例子介绍:
对于统计次数最多的元素,我们需要对数据结构进行以下操作:
具体的:
所以
实际上,有线性存储
(数组)和链式存储
(链表)两种结构,这里仅介绍链式存储。
线性表增删查:其他链表的操作与单向链表雷同,仅介绍单向链表
增加操作:
删除操作:
查找操作:
总结:
链表的查找速度慢 ( 无法用 i n d e x ) O ( n ) ,但插入和删除 ( 改变指针 ) 方便 O ( 1 ) 链表的查找速度慢(无法用index)O(n),但插入和删除(改变指针)方便O(1) 链表的查找速度慢(无法用index)O(n),但插入和删除(改变指针)方便O(1)
链表的问题常常围绕数据顺序的处理:链表反转
,快慢指针
例1.
为此,我们使用3个指针prev、curr、next,分别指向 新链表头节点、旧链表转换节点、旧链表转换节点的下一个,完成旧链表向链表逐个节点的转换。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
struct node{
int data=0;
node* next;
};
int main(){
node*head=new node,*n1=new node,*n2=new node,*n3=new node;
head->data=0;
head->next=n1; n1->data=1; n1->next=n2; n2->data=2; n2->next=n3; n3->data=3;n3->next=NULL;
node*tmp=head;
//输出原链表
while(tmp!=NULL){
cout<<tmp->data<<" ";
tmp=tmp->next;
}cout<<endl;
node* curr=head,*prev=head,*next=head->next;
head->next=NULL;
while(next!=NULL){
curr=next; next=next->next;
curr->next=prev; prev=curr;
}
//输出逆序链表
while(curr!=NULL){
cout<<curr->data<<" ";
curr=curr->next;
}
return 0;
}
/*
0 1 2 3
3 2 1 0
*/
例2.
slow走1步,fast走两步。(因为fast一次走两步,所以要防止fast到fast.next.next为空,所以while的判断条件是3个)
fast到达终点时,slow到达中点。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
typedef struct node{
int data=0;
node* next;
}*Node;
int main(){
node*head=new node,*n1=new node,*n2=new node,*n3=new node,*n4=new node,*n5=new node,*n6=new node;
head->data=0;
head->next=n1; n1->data=1; n1->next=n2; n2->data=2;
n2->next=n3; n3->data=3;n3->next=n4,n4->data=4;n4->next=n5;
n5->data=5;n5->next=n6;n6->data=6;n6->next=NULL;//1->2->3->4->5->6
//快慢指针,求链表中间值
node *fast=head,*slow=head;
while(fast!=NULL&&fast->next!=NULL&&fast->next->next!=NULL){
fast=fast->next->next;
slow=slow->next;
}
cout<<slow->data<<endl;
return 0;
}
例3.
基本思想是利用两个指针,一个快指针和一个慢指针,分别从链表头部开始遍历,快指针每次走两步,慢指针每次走一步,若快指针追上了慢指针,则说明链表存在环路;否则,当快指针到达链表尾部时,结束遍历,slow永远不可能和fast相等,链表不存在环路。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
typedef struct node{
int data=0;
node* next;
}*Node;
bool cicle(node* head){
node *fast=head,*slow=head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
return true;
}
return false;
}
int main(){
node*head=new node,*n1=new node,*n2=new node,*n3=new node,*n4=new node,*n5=new node,*n6=new node;
head->data=0;
head->next=n1; n1->data=1; n1->next=n2; n2->data=2;
n2->next=n3; n3->data=3;n3->next=n4,n4->data=4;n4->next=n5;
n5->data=5;n5->next=n6;n6->data=6;n6->next=n2;//1->2->3->4->5->6
//判断链表循环
if(cicle(head)) cout<<"Loop found"; else cout<<"No loop found"; cout<<endl;
}
顺序栈:
推荐
:用vector
模拟栈时,仅允许在线性表尾部(栈顶)插入删除数据,push_back()
和pop_back()
。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
int main(){
string str;
cin>>str;
vector<char> stk;
rep(i,0,str.size()){
if(str[i]=='['||str[i]=='('||str[i]=='{') stk.push_back(str[i]);
else if(str[i]==']' && stk.back()=='[') stk.pop_back();
else if(str[i]==')' && stk.back()=='(') stk.pop_back();
else if(str[i]=='}' && stk.back()=='{') stk.pop_back();
else {cout<<" error!";break;}
}
}
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
int main(){
vector<int> forword_stk,back_stk;
//back_stk.back()是正在浏览的页面
int n; cin>>n; int tmp;
//保存顺序浏览过的页面page 1-5
rep(i,1,6) back_stk.push_back(i);
//回退到页面n
while(back_stk.back()!=n){
tmp=back_stk.back();
forword_stk.push_back(tmp);
back_stk.pop_back();
}
cout<<"looking page "<<back_stk.back();
}
头指针front,尾指针rear
链队: 头节点仅用来表示队列(data=number),不用了存储数据
头节点的意义:给空链表的front 和rear指针一个指向,防止变成野指针。
顺序队列: 数组模拟,队尾插入时间复杂度为O(1),队头删除,后面的所有元素前移,时间复杂度为O(n),如果仅通过移动fornt指针的方式,会造成假溢出
,空间不足的情况。
实际上,上述两种解决方法都不好,假溢出
最优的解决办法是构造循环队列
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
int main(){
string a="123456";
string b="13452439";
int max_len=0;
string max_string;
rep(i,0,a.size()){
rep(j,0,b.size()){
//先找到第一个匹配的字符,判断后续字符是否匹配
if(a[i]==b[j]){
for(int m=i,n=j;m<a.size() && n<b.size();m++,n++){ //m and n are indices in a and b, respectively, so we add 1 to
if(a[m]!=b[n]) break; //to stop at the first mismatch, which is at a[m]!=b[n] (which is true if m>n)
if(max_len<m-i){
max_len=m-i;//update the maximum length so far found, which is the length of the substring starting from a[i] to the
max_string=a.substr(i,max_len);//last character of a. Note that i is not incremented. This
}
}
}
}
}
cout<<max_string<<endl;//print the substring found, which is the substring starting from a[i] to the last character of a
}
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
int main(){
string a="you are a good man";
vector<string> stk;
string tmp;
rep(i,0,a.size()){
if(a[i]==' '){stk.push_back(tmp);tmp.clear();}
else tmp+=a[i];
} stk.push_back(tmp);
while(!stk.empty()){cout<<stk.back()<<' ';stk.pop_back();}
}
非完全二叉树,使用顺序存储会浪费大量的存储空间!
递归实现二叉树的前、中、后序遍历
class node{ public: string val; node* left; node* right;};
void PreOrder(node* NODE){
if(NODE==NULL)return;
cout<<NODE->val<<" ";
PreOrder(NODE->left);
PreOrder(NODE->right);
}
void InOrder(node* NODE){
if(NODE==NULL)return;
InOrder(NODE->left);
cout<<NODE->val<<" ";
InOrder(NODE->right);
}
void PostOrder(node* NODE){
if(NODE==NULL)return;
PostOrder(NODE->left);
PostOrder(NODE->right);
cout<<NODE->val<<" ";
}
二叉查找树(二次排序树): 左小右大,左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字。中序遍历是有序数列!
利用二次排序树的性质,可以实现二分查找,加快查找速度!
!!
二叉排序树的插入:
二叉排序树删除:
例题.
可以暴力搜索,也可以用字典树,层次遍历到叶子节点的路径。
例题.层次遍历,维护OPEN表(队列),不断扩展队首子节点,加入队列。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
#include
#include
using namespace std;
// 定义二叉树结构体
struct TreeNode { int val=-1; TreeNode* left; TreeNode* right; TreeNode(int x): val(x), left(NULL), right(NULL) {}};
// 前序建立二叉树,放回根节点,如输入:1 2 3 -1 -1 -1 4 -1 -1
TreeNode* buildTree() {
int n;
cin >> n;
// 判断输入是否合法,空节点输入-1
if (n == -1) return NULL;
// 创建新节点
TreeNode* root = new TreeNode(n);
// 递归创建左右子树
root->left = buildTree();
root->right = buildTree();
return root;
}
// 层次遍历输出二叉树,1 2 4 3
void levelOrder(TreeNode* root) {
if (root == NULL) return;
// 使用队列进行层次遍历
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* cur = q.front();
q.pop();//不断取出队首节点进行子节点扩展,扩展出的新节点放入队列(OPEN表)
cout << cur->val << " ";
if (cur->left != NULL) {
q.push(cur->left);
}
if (cur->right != NULL) {
q.push(cur->right);
}
}
}
//前序遍历,1 2 3 4
void preOrder(TreeNode* root) {
if (root == NULL) return;
cout << root->val << " ";
preOrder(root->left);
preOrder(root->right);
}
int main() {
TreeNode* root = buildTree();
levelOrder(root);cout<<endl;
preOrder(root);cout<<endl;
return 0;
}
哈希冲突: 不同对象的哈希地址相同(键值对的值相同)
链地址法(将相同哈希地址的记录存放在同一条链表上)
总结:优点(CUDR飞快)、缺点(处理输入顺序敏感的问题时,会破坏序列的构造顺序)
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
#include
#include
using namespace std;
int main() {
map<string,int> database;
string s;
while(1){cin>>s;
if(s=="-1") break;
else {database[s]++;cout<<s<<" occurs "<<database[s]<<" times.\n";}
}
return 0;
}
分治需要使用递归
每轮递归的包括:分解问题、解决问题、合并结果
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
void binaryfind(int e,vector<int> arr,int low,int hight){
if(arr[(low+hight)/2] == e){cout<<"find "<<e<<endl;return;}
else if(arr[(low+hight)/2] > e) return binaryfind(e,arr,low,(low+hight)/2-1);//left half sorted
else return binaryfind(e,arr,(low+hight)/2+1,hight);//right half sorted
}
int main() {
vector<int> v={1,2,3,5,7,9,12};
binaryfind(2,v,0,v.size()-1);//1 2 3 5 7 9 12
return 0;
}
冒泡排序: 相邻元素两两比较,逆序对交换,每轮将1个大的元素交换到最后,经过多轮迭代完成排序。稳定:元素相等时不做交换
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
void bubble_sort(vector<int> &v){
rep(i,1,v.size()){
rep(j,0,v.size()-i){
if(v[j] > v[j+1])
swap(v[j],v[j+1]);
}
}
}
插入排序: 维护一个排好序的序列,不断为每个未插入的元素,与序列元素比较,找到合适的插入位置。稳定排序。
#include
using namespace std;
#define rep(i,s,e) for(int i=s;i<e;i++)
#define per(i,s,e) for(int i=s;i>e;i--)
void insert_sort(vector<int> &v){
int tmp;
rep(i,1,v.size()){
tmp=v[i];//取待排序元素v[i]
int j=i-1;
for(j=i-1;j>-1;j--){//在有序序列中,从后向前与tmp元素比较大小
if(v[j]>tmp) v[j+1]=v[j];//将所有大于tmp的元素后移一位,保持有序序列
else break;//如果tmp元素小于或等于元素j,则结束该次比较
}
v[j+1]=tmp;//将tmp元素放在合适的位置
}
}
归并排序: 将待排序序列从中点位置分成左右两个子序列,分别递归
调用归并排序函数,直到子序列长度为1。对两个已经有序的子序列进行合并,得到一个新的有序序列。 稳定排序