Frederick

Welcome to my Alter Ego's site!

Mar 25, 2025 - 9 minute read - Comments

常用板子

LIFO

用数组模拟栈

int st[N];//元素数量,栈顶下标
//压栈
st[++*st]=var1;
//取栈顶
int u =st[*st];
//弹栈,*st==0时不能继续弹出
if(*st) --*st;
//清空栈
*st =0 ;

STL中的栈

//引入stack头文件,container默认使用stf::deque
template<
     class T,class Container = std:deque<T>
>class stack;

容器必须提供以下函数,如std::vector,std::deque和std::list

  • back()
  • push_back()
  • pop_back()

stack容器常用函数

  • st.top()
  • st.push()
  • st.pop()
  • st.empty()
  • st.size()
//新建两个栈st1和st2
std:stack<int> st11,st2;
//为st1装入1
st1.push(1);
//赋值
st2=st1
//输出栈顶
cout << st2.top() << endl;

队列

FIFO

用数组模拟一个队列,用两个变量标记队列的首尾

int q[size],ql=1,qr;
插入元素:q[++qr]=x;

删除元素:ql++;

访问队首:q[ql];

访问队尾:q[qr];

清空队列:ql=1,qr=0;

双端队列

//引入<deque>头文件
template<
    class T,//数据类型
    class Allocator = std:allocator<T>//适配器
>class deque;

std::deque容器常用函数

  • 元素访问
q.front();
q.back();
  • 修改
q.push_back()
q.pop_back()
q.push_front()
q.pop_front()
q.insert()
q.erase()
  • 容器
q.empty()
q.size()

循环队列

采用循环的方式组织存放队列元素的数组,将下标为0的位置看作最后一个位置后继,如(n+1)%size

例题

一个双端队列(deque),m 个事件:

  1. 在前端插入 (w,v)
  2. 在后端插入 (w,v)
  3. 删除前端的二元组
  4. 删除后端的二元组

链表

构建链表

单向链表

struct Node{
    int value;
    Node *next;
};

双向链表

struct Node{
int value;
Node *next;
Node *right;
}

向链表插入数据

单向链表

void insertNode(int i,Node *p){
    Node *node = new Node;
    node->value = i;
    node->next = p->next;
    p->next = node;
}

单向循环链表

先判断原链表是否为空,为空则自身循环,不为空则插入数据

void insertNode(int i,Node *p){
//初始化
    Node *node = new Node;
    node->value =i;
    node->next = NULL;
//判断是否为空
    if(p == NULL){
        p = node;
        node->next = node;//指向自己
    }else{
       node->next = p->next;
       p->next=node;
    }   
}

双向循环链表

除了要判断给定链表是否为空外,还要修改左右指针

void insertNode(int i,Node *p){
    Node *node =new node;
    node->value=i;
    if(p == NULL){
        p = node;
        node->left=node;
        node->right=node;
    }else{
     node->right = node->right;
     node->left=node;
     p->right->left=node;
     p->right=node;
    }
}

从链表中删除数据

单向链表

利用虚拟节点

删除 t。此时虽然原结点 p 的地址还在使用,删除的是原结点 p->next 的地址,但 p 的数据被 p->next 覆盖,p 名存实亡。

void deleteNode(Node *p){
   p->value = p->next->value;//保存p下一个结点的值
   Node *t = p->next;//利用虚拟结点保存p->next
   p->next = p->next->next;
   delete t;
}

双向循环链表

void deleteNode(Node *p){
    p->left->right=p->right;
    p->right->left=p->left;
    Node *t =p;
    p=p->right;//将 p 的右节点地址赋给 p,以避免 p 变成悬垂指针;
    delete t;
}

存储

父节点parent[N]

左右兄弟

int v=child[u];

while(v!=EMPTY_NODE){
//...
  v=sib[v];

}

二叉树,记录每个节点的左右子节点

int parent[N];
int lch[N],rch[N];
//
int child[N][2];

先序遍历(根,左,右)

void preorder(BiTree* root){
  if(root){
    cout << root->key<< "";
    preorder(root->left);
    preorder(root->right);
  }
}

中序遍历(左,根,右)

void inorder(BiTree* root){
  if(root){
    inorder(root->left);
    cout<<root->key<<"";
    inorder(root->right);
  }
}

后序遍历(左,右,根)

void postorder(BiTree* root){
  if(root){
  postorder(root->left);
  postorder(root->right);
  count<<root->key<<"";
  }
}

BFS遍历

vector<vector<int>> levelOrder(Node* root){
  if(!root){
    return {};
  }
  vector<vector<int>> res;
  queue<Node*> q;//队列
  q.push(root);//将根节点加入队列
  while(!q.empty()){
    int currentLevelSize = q.size();//获取当前层节点数
    res.push_back(vector<int>()); //为当前层创建新的向量
    //处理当前层的所有节点
    for(int i =0;i<currentLevelSize;++i){
      Node* cur = q.front(); //获取队首节点
      q.pop(); //弹出队首节点
      res.back().push_back(cur->val);//将节点值加入当前层
      //将子节点加入队列
      for(Node* child:cur->children){
        q.push(child);
      }
    }
  }
}

Morris遍历

Morris 遍历的实质是避免使用栈,利用底层节点空闲的 right 指针指回上层的某个节点,从而完成下层到上层的移动。

void mooris(TreeNode* root){
  TreeNode* cur = root;
  while(cur){
    if(!cur->left){
    std::cout<<cur->val<<" ";
    cur=cur->right;
    continue;
    }
    //找到当前节点的左子树的最右节点
    TreeNode* mostRight = cur->left;
    while (mostRight->right && mostRight ->right !=cur){
      mostRight = mostRight->right;
    }
    if(!mostRight->right){
    //如果最右节点的right指针为空,将其指向当前节点,并进入左子树
      mostRight -> right =cur;
      cur = cur ->left;
    }else{
    //如果最右节点的right指针指向当前节点,说明左子树已经遍历完毕,输出当前节点的值并进入右子树
    mostRight->right =nullptr;
    std::cout << cur->val << " ";
    cur = cur->right;
    }
  }
}

排序

稳定性?指相等的元素经过排序之后相对顺序是否发生了改变

选择排序

用数组实现不稳定

每次找出第i小的元素,然后将这个元素与数组第i个位置上的元素交换

#include <utility>
void selection_sort(int* a,int n){
    for(int i=1;i<n;++i){
      int ith = i;
      for(int j=i+1;j<=n;++j){
        if(a[j]<a[ith]){
          ith =j;
        }
      }
      std::swap(a[i],a[ith]);
    }
}

每次遍历i=1-n,固定i,遍历后面所有数(j),如果后面数比前面数小,记住这个数的位置,结束后将他们交换

冒泡排序

稳定

每次检查相邻两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。

最坏情况,要会执行n-1+n-2+……+1次交换操作

void bubble_sort(int *a,int n){
    bool flag =true;
    while(flag){
    flag= false;
    for(int i=1;i<n;++i){
      if(a[i]>a[i+1]){
        flag=true;
        int t=a[i];
        a[i]=a[i+1];
        a[i+1]=t;//交换
      }
    }
   }
}

QA:为什么flag放在循环里面?

只要找到不满足条件的立即停止

插入排序

稳定,最优n,最坏n^2

插入排序(英语:Insertion sort)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为「已排序」和「未排序」两部分,每次从「未排序的」元素中选择一个插入到「已排序的」元素中的正确位置。

一个与插入排序相同的操作是打扑克牌时,从牌桌上抓一张牌,按牌面大小插到手牌后,再抓下一张牌。

void insertion_sort(int arr[],int len){
    for(int i =1;i<len;++i){
        int key = arr[i];
        int j =i-1;
        while(j >=0 && arr[j]>key){
            arr[j+1]=arr[j];
            j--;
        }
        arr[j+1]=key;
    }
}

从第1个元素开始排序,再取第二个元素,和第一个元素比较,排序,再取第三个元素,和刚跟排序好的最后一个元素比较……

计数排序

线性时间,稳定

计数排序的工作原理是使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组C来讲A中的元素排到正确的位置

  • 计算每个数出现了几次;

    求出每个数出现次数的前缀和;

    利用出现次数的前缀和,从右至左计算每个数的排名。

n是数组长度,w是元素值域范围/最大值

#include <cstring>
constexpr int MAXN =1010;//数组最大长度
constexpr int MAXW = 100010;//元素至于范围
int cnt[MAXW];//计数数组,用于统计每个元素出现的次数
int b[MAXN];//输出数组,存储排序后的结果
int* counting_sort(int* a,int n,int w){
    memset(cnt,0,sizeof(cnt));//初始化计数数组
    for(int i=0;i<=n;++i) ++cnt[a[i]];//统计每个元素出现的次数
    for(int i=1;i<=w;++i) cnt[i]+=cnt[i-1];//计算前缀和,确定每个元素最后出现的的位置
    for(int i=n;i>=1;--i) b[cnt[a[i]]--]=a[i];//从后向前遍历,将元素放到正确位置
}

输入数组 a: [0, 2, 1, 3, 2, 1] n = 5, w = 3

步骤1:统计次数 cnt[0] = 1 cnt[1] = 2 cnt[2] = 2 cnt[3] = 1

步骤2:计算前缀和 cnt[0] = 1 cnt[1] = 3 cnt[2] = 5 cnt[3] = 6

步骤3:从后向前放置元素 i = 5: a[5] = 1 cnt[1] = 3 b[3] = 1 cnt[1] = 2

i = 4: a[4] = 2 cnt[2] = 5 b[5] = 2 cnt[2] = 4

…以此类推

最终结果:b = [1, 1, 2, 2, 3]

MSD基数排序

基于 k - 关键字元素的比较方法,可以想到:先比较所有元素的第 1 关键字,就可以确定出各元素大致的大小关系;然后对 具有相同第 1 关键字的元素,再比较它们的第 2 关键字……以此类推。

void MSD_radix_sort(u32ptr first,u32ptr last){

}

通常而言,基数排序比基于比较的排序算法(比如快速排序)要快。但由于需要额外的内存空间,因此当内存空间稀缺时,原地置换算法(比如快速排序)或许是个更好的选择。

快速排序

不稳定,平均 O(nlogn),最坏 O(n²)

分治思想

  • 将数列划分为两部分

  • 递归到两个子序列中分别进行快速排序

  • 不用合并,此时数列已经有序

struct Range{
    int start,end;
    Range(int s=0,int e=0){
    start =s,end =e;
    }
};
template <typename T>
void quick_sort(T arr[],const int len){
   if(len<=0) return;
   Range r[len];//使用数组模拟栈
   int p=0;     //栈顶指针
   r[p++]=Range(0,len-1);//压入整个数组范围
   while(p){//当栈不为空
   Range range = r[--p];//弹出栈顶范围
   if (range.start >= range.end) continue;//范围无效跳过
   T mid = arr[range.end];//选择最右边的元素作为基准值
   int left = range.start,right=range.end-1;
   //分区过程
   while(left<right){
   while(arr[left]<mid && left<right) left++;
   while(arr[left])>=mid&&left<right) right--;
   std:swap(arr[left],arr[right]);//左边小就不交换,比较下一个,右边同理
   }
   //处理基准值
   if(arr[left]>=arr[range.end])
       std::swap(arr[left],arr[range.end]);
   else
       left++;
   }
   r[p++]=Range(range.start,left-1);
   r[p++]=Range(left+1,range.end);
}

选一个基值,一个左指针,一个右指针,左指针指到的数字比基准小就不处理,左指针右移动,否则交换后,左指针右移动,然后右指针,数字大不处理,小的话交换,左移,当左右指针相遇,取这个数和最右边的元素比较,再次重复

归并排序

最优,平均,最坏情况下均为nlogn,空间复杂度为n

比较两个数组每个元素(有序),把小的放入新数组,如果还有剩下没比较的全部放入(因为有序)

void merge(const int *a,size_t aLen,const int *b,size_t bLen,int *c){
  size_t i=0,j=0,k=0;
  while(i<aLen && bLen){
       if(b[j]<a[i]){
       c[k]=b[j];
       ++j;
       }else{
       c[k]=a[i];
       ++i
       }
       ++k;
  }
  for(;i<aLen;++i,++k) c[k]=a[i];
  for(;j<bLen;++j,++k) c[k]=b[j];
}

分治法实现归并排序

mid=(l+r)/2

void merge_sort(int *a,int l,int r){
  if(r-l<=1)  return;
  int mid = l+((r-l)>>1);//每次都加总长度的1/2
  merge_sort(a,l,mid),merge_sort(a,mid,r);
  //合并
  int tmp[1024]={};
  merge(a+l,a+mid,a+r,a+mid,tmp+l);
  for(int i=l;i<r;++i) a[i]=tmp[i];
}

堆排序

不稳定,时间复杂度nlogn

1.建堆

iParent(i)=(i-1)/2
iLeftChild(i)=2*i+1
iRightChild(i)=2*i+2

2.排序

void sift_dowmn(int arr[],int start,int end){
   int parent = start;
   int child =parent*2+1;
   while(child<=end){
   //先比较两个子节点大小,选择最大的
   if(child+1<=end&&arr[child]<arr[child+1]) child++;
   //如果父节点比子节点大,代表调整完毕,直接跳出函数
   if(arr[parent]>=arr[child])
       return;
   else{//否则交换父子内容,子节点在和孙节点比较
       swap(arr[parrent],arr[child]);
       parent = child;
       child = parent*2+1;
   }
   }
}
void heap_sort(int arr[],int len){
  //从最后一个节点的父节点开始
  for(int i=(len-1-1)/2;i>=0;i++) sift_down(arr,i,len-1);
  for(int i = len-1;i>0;i--){
  swap(arr[0],arr[i]);
  sift_down(arr,0,i-1);
  }
}

桶排序

稳定,时间平均复杂度n+n*n/k+k,最坏n^2

1.设定一个定量的数组当作空桶

2.遍历,放入相应桶

3.对非空桶内部排序

4.把非空桶中的元素拿出

constexpr int N=100010;
int n,w,a[N];//n是元素个数,w是值域范围,a是待排序数组
vector<int> bucket[N];
//排序函数
void insertion_sort(vector<int>&A){
  for(int i=1;i<A.size();++i){
      int key=A[i];
      int j=i-1;
      //将比key大的元素后移
      while(j>=0&&A[j]>key){
      A[j+1]=A[j];
      --j;
      }
      A[j+1]=key;//插入key
  }
}
void bucket_sort(){
    //计算每个桶的大小
    int bucket_size=w/n+1;
    //清空所有桶
    for(int i=0;i<n;++i){
        bucket[i].clear();
    }
    //将元素分配到桶中
    for(int i=1;i<=n;++i){
       bucket[a[i]/bucket_size].push_back(a[i]);
    }
    //对每个桶排序合并结果
    int p=0;
    for(int i=0;i<n;++i){
        insertion_sort(bucket[i]);
        for(int j=0;j<bucket[i].size();++j){
           a[++p]=bucket[i][j];
        }
    }
}

二分法

int binary_search(int start,int end,int key){
  int ret =-1;//未搜索到数据返回-1下标
  int mid;
  while(start<=end){
    mid = start+((end-start)>>1);//直接取平均可能会溢出
    if(arr[mid]<key)
      start = mid + 1;
    else if(arr[mid]>key)
      end = mid-1;
    else{
        ret = mid;
        break;
    }
  }
  return ret;
}

三分法

如果需要求出单峰函数的极值点

while(r-l>eps){
  mid = l+((r-l)>>1);
  lmid = mid-eps;
  rmid = mid+eps;
  if(f(lmid)<f(rmid))
    r = mid;
  else
    l = mid;
}

DFS(图论)

用栈来实现

时间复杂度位n+m,空间复杂度为n,n表示点数,m表示边数

DFS(v) // v 可以是图中的一个顶点,也可以是抽象的概念,如 dp 状态等。
  在 v 上打访问标记
  for u in v 的相邻节点
    if u 没有打过访问标记 then
      DFS(u)
    end
  end
end

非递归版本

vector<vector<int>> adj;//邻接表
vector<bool> vis;//记录节点是否已经遍历

void dfs(int s){
    stack<int> st;
    st.push(s);
    vis[s]=true;
    
    while(!st.empty()){
        int u=st.top();
        st.pop();
        
        for(int v:adg[u]){
        if(!vis[v]){
            vis[v]=true;
            st.push(v);
        }
        }
    }
}

递归实现

vector<vector<int>> adj;//邻接表
vector<bool> vis;//记录节点是否已经遍历

void dfs(const int u){
     vis[u]=true;
     for(int v:adj[u])
     if(!vis[v]) dfs(v)
}

例题:把正整数n分解成3个不同的正整数,排在后面的数必须大于等于前面的数

int m,arr[103];//m是划分的最大个数,arr[103]存储当前划分方案
void dfs(int n,int i,int a){//n是待划分的数,a是可以取到的最小值
  if(n == 0){//划分完了,找到以恶搞完整的划分方案,遍历打印
     for(int j=1;j<=i-1;++j){
         printf("%d",arr[j]);
     printf("\n");
     }
  if(i<=m){
    for(int j=a;j<=n;++j){//从a开始尝试每个可能的数
    arr[i]=j;//记录当前选择
    dfs(n-j,i+1,j);//递归处理剩余部分
    }
  }
  }
}

NKUwiki爬虫说明 一些对于C++的思考

comments powered by Disqus