数据结构总结

数据结构

目录

  • 数据结构
    • 时间和空间
    • 线性表
      • 1.数组
      • 2.链表
    • 栈和队列
      • 1.栈
      • 2.队列
    • 二叉树
      • 1.基本概念
      • 2.三种遍历
      • 3.两种优先
      • 3.二叉查找树BST
      • 4.堆heap
      • 5.哈夫曼树
      • 6.普通树
    • 查找
      • 1.哈希表
      • 2.二分查找
    • 索引
      • 1.基本概念
      • 2.2-3树
      • 3.B树
      • 4.B+树
      • 1.基本概念
      • 2.两种优先遍历
      • 3.拓扑排序
      • 4.最短路径
      • 5.最小生成树
    • 排序
      • 1.基本介绍
      • 2.三个O(n^2)的排序
        • 1.插入排序
        • 2.冒泡排序
        • 3.选择排序
      • 3.希尔排序shell
      • 4.快速排序【重点】
      • 5.堆排序
      • 6.归并排序
      • 7.基数排序
      • 8.计数排序(了解)

时间和空间

  1. 三界

    1. **O()**是最紧上界
    2. Ω()是下界
    3. **θ()**是精确界
  2. 加法规则:O(g1)+O(g2) = O(max{g1,g2})

  3. 嵌套相乘:O(g1*g2)

  4. 一般大小比较:O(logn)2)n)

  5. 简单案例:

    for(int j=1;j<=n;j*=2){}
    //j每次都要乘2,设第k次跳出循环,则要2^k>n,求解k即可
    //在某些案例中:++i比i++快!由于是先保留后计算和先计算后保留的区别
    

线性表

1.数组

2.链表

  1. 单向链表

    案例:若要删去1个节点,如何删去p指向的c节点,将d复制给c,再删去d节点
    a->b->c->d
    
  2. 双向链表

  3. 循环链表

栈和队列

1.栈

  1. 特点:先进后出
  2. 应用:
    1. 表达式求值——两个栈,一个算符栈(栈顶运算符优先级高),一个操作数栈(对应两个操作数)
    2. 括号匹配——检验【(】,括号左边先进栈,右边后进栈匹配
    3. 八皇后
    4. 迷宫,递归,函数调用
  3. 递归容易导致栈溢出!

2.队列

  1. 特点:先进先出
  2. 解决队空和队满:
    1. 设置标志
    2. 设一个变量
    3. 少用一个内存maxsize-1
  3. 循环队列(个人理解)
    1. 模运算:(i+1)%size
    2. 设置rear=0,front=1
      1. 若队空,则rear
      2. 若队满,经过(i+1)%size,从index=1开始存,最终rear=front

二叉树

1.基本概念

  1. 深度:节点最大层次,从根节点开始

    //求深度
    int depth(node* p) {
     	//结束条件,都是因为上一层传进了左子树或右子树
     	if (p == NULL) return 0;
     	else {
      		int left = 0;
      		int right = 0;
     		left = depth(p->lc);//先一直找左子树,直到叶子节点时,才执行查找右子树并返回
      		right = depth(p->rc);
      		return left > right ? (left + 1) : (right + 1);
     	}
    }
    
  2. 存储:

    1. 数组:一般完全二叉树
      1. 当前索引为index时,左孩子为2*index+1;右孩子为2*index+2
      2. 父节点(index-1)/2
    2. 链式
  3. 规律

    1. 二叉树的第 i 层上至多有 2^(i-1) 个结点
    2. 深度为k,至多有2^k-1个节点;至少k个

2.三种遍历

  1. 先序

    void preOrder(node* tree) {
    	if (tree == null) return;
    	cout << tree->ch;
    	preOrder(tree->lc);
    	preOrder(tree->rc);
    }
    
  2. 中序

    void inOrder(node* tree) {
    	if (tree == null) return;
    	preOrder(tree->lc);
        cout << tree->ch;
    	preOrder(tree->rc);
    }
    
  3. 后序

    void postOrder(node* tree) {
    	if (tree == null) return;
    	preOrder(tree->lc);
    	preOrder(tree->rc);
        cout << tree->ch;
    }
    

3.两种优先

  1. 广度优先

    //队列
    void BFS_tree(binTreeNode* p) {
    	queue q;
        q.push(p);
        binTreeNode* temp=null;
        while(!q.empty){
            temp=q.front();//取前节点
            visit(temp);//输出前节点的全部节点!!!
            q.pop();//将前节点弹出
            //左右孩子非空
            if(temp->left!=null) q.push(temp->left);
            if(temp->right!=null) q.push(temp->right);
        }
    }
    
  2. 深度优先(非递归)

    //栈,递归可用先序
    //进栈的同时访问,到最底层后出栈返回
    void DFS_tree(binTreeNode* p) {
    	stack s;
        s.push(p);
        binTreeNode* temp=null;
        while(!s.empty()){
            temp=stack.top();//取最高的节点
            visit(temp);//输出节点的其中一个节点
            stack.pop();//将已输出的弹出
            //左右孩子非空
            if(temp->left!=null) stack.push(temp->left);
            if(temp->right!=null) stack.push(temp->right);
        }
    }
    

3.二叉查找树BST

  1. 规则:左子树比根小,右子树比根大

  2. 注意中序遍历得到的数据

  3. 删除操作!

    void remove(node* root, int key) {
        if (root == NULL) return;
        else if (key < root->data) remove(root->left, key);
        else if (key > root->data) remove(root->right, key);
        else {//等于,即为找到!
            if (root->left == NULL) root = root->right;//被删节点若左孩子为空,直接用右孩子补上
            else if (root->right == NULL) root = root->left;
            else {//左右孩子都非空,此时可以找被删节点左孩子中最大或右孩子中最小进行补
                node* temp = findv(root->right, root);
                root->data = temp->data;
                delete temp;
            }
        }
    }
    node* findv(node* root, node* preRoot) {//右孩子中最小
        if (root->left == NULL) {
            if (root->right != NULL) preRoot->left = root->right;//要往上提的节点若非叶子时
            return root;
        }
        findv(root->left, root);
    }
    
  4. 查找操作

    bool search(node* root,int key){
        while(root!=null){
            if(key==root->data) return true;
            else if(keydata) root=root->left;
            else root=root->right;
        }
        return false;
    }
    
  5. 插入操作

    //非递归
    void insert(int key, node* root) {
        node* prev=null;//定位最终插入位置的父节点
        //1.先定位
        while(root!=null) {
            prev=temp;
            if(key < temp->data) temp=temp->left;
            else if(key > temp->data) temp=temp->right;
            else return;
        }
        //2.再插入
        if(key < prev->data) {//创建新节点} 
        else{//创建新节点}
    }
    //递归(不严谨)
    void insert(int key, node* root) {
        if(root==null) {
            //先创建new,赋值key
            return;
        }
        if(key < root->data) {
            insert(key,root->left)
        }else {
            insert(key,root->right)
    	}
        return;
    }
    

4.堆heap

  1. 基本概念

    1. 一般用数组存储(存储的是节点)是完全二叉树!
    2. 所有叶子节点的顺序号 i 有 2*i+1>=n
  2. 小顶堆(根最小)

    1. 下面的 siftdown函数 和 sift_heap 函数一样原理

    2. 建立堆(数组中)

      void sift_heap(int a[], int parent, int last) {
      	for (int lc = parent * 2 + 1; lc <= last; lc = lc * 2 + 1) {
      		//左,右节点最小的一个
      		if (a[lc] < a[lc + 1] && lc + 1 <= last) lc++;
      		//如果父节点比孩子大就交换,且孩子的索引要小于等于数组的元素个数(最后的一个叶子)
      		if (a[parent] < a[lc]) {
      			swap(a[parent], a[lc]);
      			parent = lc;//当前层的孩子作为下一层的父节点
      		}
      		else break;//不需要交换
      	}
      }
      void build_heap(int a[], int nums) {
      	for (int node = nums / 2 - 1; node >= 0; node--) {//从最后一个非叶子节点node开始
      		sift_heap(a, node, nums - 1);
      	}
      }
      
    3. 堆删除元素

      bool remove(int pos) {
      	if(pos=<0||pos=>=n) return false;
          swap(heap[pos],heap[--n]);//将要删除的元素放在数组的最后(即是最后一个叶子)
          while(pos!=0 && heap[pos]left != null || heap[pos]->right != null) {//不是叶子
              //将当前节点和左右孩子比较
              int lc=2*pos+1;
              int rc=2*pos+2;//注意要满足是否有右孩子,rcheap[rc]) lc=rc;//rc更小,更小的和父节点比较
              if(heap[pos] <= heap[lc]) return;
              swap(heap[pos],heap[lc]);
              pos=lc;
          }
      }
      
    4. 堆插入

      bool insert(int key) {
          if(pos_index >= size) return false;//堆满时
          int current=pos_index++;//在数组中,先放在最后一个
          heap[current]=key;
          //开始调整堆,保证 插入节点的父节点 比key要小
          while( current!=0 && heap[current]
  3. 大顶堆(根最大):与小顶堆类似

5.哈夫曼树

  1. 利用结构数据储存,越靠近叶子,权值越低

  2. 每次从哈夫曼编码中,选择两个权值最小的编码进行创建节点(两个合并一个后继续加入原编码中继续比较!)

  3. 建立二叉树后,从根节点遍历,左孩子路径上一般为0,右孩子一般为1

  4. 简单案例

    //假设有n个哈夫曼编码(注意数组中index=0不存!)
    struct node {
        int parent = 0;//判断是否已经合并
        int lc=0;
        int rc=0;
        char ch=0;//编码
        int weight=0;//权值
    };
    void huffman() {
        node* arr=new node[2*n];//n个编码,2n-1数组大小,因为两个合并为一个后,也要放进数组中
        //假设arr的前n个已经存有编码信息(index=0不存放),开始创建
        for (int i = n + 1; i <= 2 * n - 1; ++i) {
             int index1 = 0;
             int index2 = 0;
             selectMin(arr, i - 1, index1, index2);//从arr数组中选两个权值最小的,从1到i-1之间
             arr[index1].parent = i;
             arr[index2].parent = i;//parent被标记,说明已经合并
             arr[i].lc = index1;
             arr[i].rc = index2;
             arr[i].weight = arr[index1].weight + arr[index2].weight;
         }
    }
    //求每个编码的路径边数,也就是重新分权值,1到n,n个编码
    for(int i=1; i<=n; i++) {
        int j=i;
        while (arr[j].parent != 2*n-1) {//此时索引2n-1是整棵树的根节点!
             ++count;//边数,从0开始
             j = arr[j].parent;//从叶子往根节点找!
        }
    }
    

6.普通树

  1. 一个根节点有多个孩子
  2. 表示方式:左孩子右兄弟(数组或链表);父节点数组;子节点链表

查找

1.哈希表

  1. 通常在数组中应用
  2. 计算方法:取模法;平方取中
  3. 解决存储冲突
    1. 开散列:开额外空间,指针链表
    2. 闭散列:每个哈希后index对应多个内存,在内部找其他空间存储
      1. 桶哈希:将原本的一个B作为一个index,改为多个B作为一个index;
      2. 探测:直线探测(偏移量p(k,i)=i),跳跃探测p(k,i)=c*i,非线性跳跃p(k,i)=i^2
    3. 常用的是直线探测
  4. 计算公式:position = ( hash(key) + p(k,i) )%M
    1. M:一般指的是数组大小
    2. hash(key):对要存储的元素进行hash运算,一般取key mod M
    3. p(k,i):解决冲突
    4. 若找不到存储空间,不能无限hash下去,要设置限制停下来,一般限制次数不能大于M
  5. 删除元素:在删除处作标记,可以让其他元素查找时,跳过标记;插入标记的元素可被覆盖

2.二分查找

  1. 数组已排序

  2. 比较次数 <= 树的深度[log(2)n]+1(2为底数)

  3. 不停通过与中间的元素比较进行减半查找

    //非递归
    bool bin_search(int key) {//已知数组arr
        int low=0;
        int high=arr.length;
        while(low<=arr.length){
            int mid=(low+high)/2;
            if(arr[mid]==key) return true;
            else if(arr[mid]

索引

1.基本概念

  1. 提供更高效率的查找,插入,删除
  2. 提供更多的查找key
  3. 分为线性索引(若索引内存不足,则可以对索引再建索引),树索引!

2.2-3树

  1. 基本概念:

    1. 每个节点至多2个数(如A,B)
    2. 每个节点至多3个子节点(如B)
  2. 要求规则

    1. 树的高度保持一致,叶子处于同一层!
    2. 插入元素是,一定要到叶子处插入(叶子元素未满时,要重新排序)
    3. 当要插入节点的元素满时,叶子进行分裂中间大的节点往父节点塞进(父节点元素重新排序!);若父节点也满,父节点分裂往上塞,直到有空节点。
    4. 向上生长,根节点高度+1
  3. 查找:2-3树是3叉树,高度为θ(log(3)n)

    //假设节点中左值名为lkey,右值名为rkey,根节点root,查找key
    if(root==null){}
    if(key==lkey){}
    if(root->rkey!=null && key==rkey){}
    if(key
  4. 2-3树的插入

    1. 三种情况

      1. 叶子元素未满
      2. 叶子元素满,但父节点未满
      3. 叶子和父节点元素都满
    2. 简单思路

      //假设节点中左值名为lkey,右值名为rkey,根节点root,查找key,returnp是叶子满时返回父节点的一个节点
      bool insert(node* returnp,……){
      	if(root==null){}
      	else if(isLeaf(root)){
          	if(root->rkey==null){}//与插入rkey处重新排序
          	else{splitNode(returnp,……);}//叶子满分裂
      	}
      	else if(keylkey){}//递归左
      	else if(root->rkey==null || keyrkey){}//递归中间孩子
      	else{}//递归右
          if(returnp!=null){//说明叶子满
              if(root->rkey!=null){splitNode(returnp,……);}//此时root为父节点
              else{//插入右值,调整父节点指针指向
                  if(returnp->datalkey){}//data为返回节点的数据
                  else{}
              }
          }
      }
      //分裂,简单说明
      bool splitNode(node* retptr,int key,……){//key为插入值,retptr为返回的节点
          if(key

3.B树

  1. 基本概念
    1. B树高度平衡,叶子同一层
    2. 一般每个节点满足磁盘一次存取的数量
    3. 2-3树的扩展,为了减少树的高度
  2. m阶B树:子节点最多为m个
    1. 根要么是一个叶子,要么至少有两个子节点
    2. 除根节点外,每个内部节点都有 [m/2] 到 m 个子节点
    3. 节点最多有m个子节点,m-1个元素

4.B+树

  1. 基本概念
    1. 所有数据放在叶子节点;叶子间,非根节点间进行链接
    2. 父节点只放关键字索引(非索引)
    3. 查找终结于叶子
  2. 规律
    1. 叶子一般元素个数[m/2] 到 m
    2. 非根与非叶子有 [m/2] 到 m 个子节点,最多 [m/2] 到 m 个元素
    3. 查找,插入,删除(分裂和合并)
    4. 分裂后,将新增叶子中的第一个数据重写放进父节点后,再重现父节点链接

1.基本概念

  1. 领接矩阵(完全图):便于找关系,直观;但浪费空间
  2. 领接表(稀疏图):便于省内存;但查找两点关系麻烦

2.两种优先遍历

  1. 深度优先DFS【递归】

    1. 每次选领接顶点时,有一定规则(如选所有领接节点中数据最小的优先DFS)

    2. 每次进入递归时,从当前顶点遍找到所有的领接节点,逐个递归,并且检测当前顶点是否已经访问

    3. 简单案例

      //假设n个顶点的名称为0,1,2,……,n-1
      struct GraphMatrix
      {
      	int** matrix;//领接矩阵
      	int* mark;//记录每个节点是否被访问,初始化全0,mark[0]=1,表示顶点0已被访问
      };
      void DFS_graph(GraphMatrix g,int peak,int n){
          cout<
  2. 广度优先BFS【队列】

    1. 每次从头顶点开始,当前顶点的邻居全部入队

    2. 当队列不为空时,出队一个顶点,并访问这个顶点,该顶点的 不被访问过的邻居顶点 入队

    3. 简单案例

      //假设n个顶点的名称为0,1,2,……,n-1
      struct GraphMatrix
      {
      	int** matrix;//领接矩阵
      	int* mark;//记录每个节点是否被访问,初始化全0,mark[0]=1,表示顶点0已被访问
      };
      void BFS_graph(GraphMatrix g,int peak,int n){
      	deque queue;//队列
          queue.push_back(peak);
          while(!queue.empty()){//非空
              int front=queue.front();
              q.pop_front();//出队
              cout<

3.拓扑排序

  1. 基本概念

    1. 有向图
    2. 可应用于判断有向图是否构成环
    3. 重点:每取去一个顶点,对应的入度-1输出 入度为0 的点
  2. 简单应用:判断是否有环

    //在领接矩阵中,有向图,这里设:行为出度,列为入度
    deque queue_v;//顶点队列
    int* degree = new int[v_nums + 1];//顶点入度数组
    int** a = new int* [v_nums + 1];//顶点标号为1到n,v_nums为顶点个数
    //计算每个顶点的入度
    for (int i = 1; i <= v_nums; i++){
    	for (int j = 1; j <= v_nums; j++) {
    		if (a[j][i] == 1) degree[i]++;//按列查找入度
        }
    	if (degree[i] == 0) queue_v.push_back(i);//0入度的节点入队!!!
    }
    //0入度的节点出队,入度减1
    while (!queue_v.empty()){
    	int index = queue_v.front();
    	for (int i = 1; i <= v_nums; i++) {//找到该节点的出度指向,并且被指向的节点,入度减1
    		if (a[index][i] == 1) degree[i]--;
    		if (degree[i] == 0) queue_v.push_back(i);//如果入度减少后,等于0,就入队
        }
    	queue_v.pop_front();//出队
    }
    //查找入度数组是否全为0
    int nums = 0;
    for (int i = 1; i <= v_nums; i++) {
    	if (degree[i] != 0) nums++;//若还有顶点入度不为0,则说明有环
    }
    

4.最短路径

  1. Floyd算法(多元顶点)

    1. 思路(n个顶点):

      1. 每次拿一个顶点多为中转点temp(如路径 ab+bc 与 ac 比较大小,其中b为a到c的中转点);n个顶点都要作中转所以循环n次
      2. 在其中一个顶点作为中转点temp后,计算除该中转点的其他顶点的最短路径(必须经过中转点);也就是比较(vk1,vk2)与(vk1,temp)+(temp,vk2)的大小;一个顶点循环n次
      3. 在第2步后,有n个顶点,又要循环n次。总共n^3次。
    2. 简单案例

      //图结构参考Dijkstra算法
      void Floyd(GraphMatrix g) {
          for(int k=1; k<=n; k++) {//中转点
              for(int i=1; i<=n; i++) {//从一个顶点出发经过中转点
                  for(int j=1; j<=n; j++) {
                      if(g.matrix[i][j] > g.matrix[i][k] + g.matrix[k][j]) {
                          g.matrix[i][j] = g.matrix[i][k] + g.matrix[k][j];
                      }
                  }
              }
      	}
      }
      
  2. Dijkstra算法(单元顶点)

    1. 思路:求源点到终点的最短路径

      1. 初始化:先找出从源点v0 到 各终点v的直达路径(v0,vk),即仅通过一条弧到达的路径。
      2. 选择:从直达路径中找出一条长度最短的路径(v0,vk_min),并以这条路径的另一个顶点vk_min作为源点
      3. 更新:然后对其余各条路径进行适当调整:若源点为起点,在图中存在弧(vk_min,vk),且(v0,vk_min)+(vk_min,vk) < (v0,vk),则以路径之和替换(v0,vk)。
      4. 在调整后的各条路径中,再找长度最短的路径,依此类推。
    2. 简单案例

      //假设n个顶点的名称为1,2,……,n
      struct GraphMatrix
      {
      	int** matrix;//领接矩阵,无法到达用INT_MAX表示
      	int* mark;//记录每个节点是否被访问,初始化全0,mark[1]=1,表示顶点1已被访问
      };
      //找最短路径数组最小的(除访问过外)
      int search_min_index(GraphMatrix g, int* shortest_path, int n , int begin) {
      	int min = INT_MAX;
      	int index = 0;
      	for (int i = 1; i <= n; i++)
      	{
              //找最小时,既要排除已经标志的点,又要不能等于自身!
      		if (shortest[i] < min && i != begin && g.mark[i]!=1) {
      			min = shortest[i];
      			index = i;
      		}
      	}
      	return index;//返回当前最小路径的顶点
      }
      void Dijkstra(GraphMatrix g, int n, int begin, int target) {
          //最短路径初始化
          int* shortest_path = new int[n + 1];//建立最短路径数组,顶点名称是1到n,直接用索引替代
          shortest_path[begin]=0;
          for(int i=1; i<=n; i++) {
              if(i!=begin) shortest_path[i]=INT_MAX;
          }
          //标记是否被访问的数组 初始化
          g.mark[begin]=1;
          //从源点出发,找到与其相邻的顶点,进行最短路径更新
          for(int i=1; i

5.最小生成树

  1. Prim算法

    1. 思路:

      1. 假设有一个生成树集合G,任选一个顶点a加入集合G{a}
      2. 集合中所有顶点出发,找到一条最短边,边的另一端顶点b加入集合G(且顶点b是未被访问过的)
      3. 循环上面操作,直到所有顶点被访问
    2. 局部贪心但全局最优;从点出发扩边

    3. 简单案例

      #define max 100
      #define Weight_max 10000
      //顶点名称从1到n,这里默认顶点1为源点!
      void Prim(int g[][max], int v_num) {
          //weight_Arr[j],记录在邻接矩阵中第j列的权重g[i][j]的值,当值为-1说明已经在最小生成树里面
      	int weight_Arr[max];
          //parent_Vex[i],记录weight_Arr[j]的父顶点,也就是g[i][j]中的i——若没有父顶点,则为-1
      	int parent_Vex[max];
      	int min, sum=0, min_Vex=1;
      	for (int i = 2; i <= v_num; i++) {
      		weight_Arr[i] = g[1][i];//注意初始化,全部赋值
      		parent_Vex[i] = 1;//本函数的源点!下标为1开始
      	}
      	parent_Vex[1] = -1;//由于下标为1时是源点,则源点没有父顶点,所以赋值为-1
      	for (int i = 2; i <= v_num; i++) {
      		min = Weight_max;//初始化
      		min_Vex = 1;//初始化
      		for (int j = 2; j <= v_num; j++) {
      			//找到权重最小的顶点,且没进入生成树
      			if (weight_Arr[j] < min && weight_Arr[j] != -1) {
      				min = weight_Arr[j];
      				min_Vex = j;//【重点】找到最小权值的边后,将这条边的终点作为下一次查找的开始
      			}
      		}
      		sum += min;
      		weight_Arr[min_Vex] = -1;//赋值为-1,说明已经进入生成树!
      		for (int i=2; i <= v_num; i++) {
      			//更新表,看 以新的顶点到达某点A 与 旧的顶点 到达顶点A 哪个权值小(权值大不换)
      			if (g[min_Vex][i] < weight_Arr[i]) {
      				weight_Arr[i] = g[min_Vex][i];
      				parent_Vex[i] = min_Vex;//修改父顶点
      			}
      		}
      	}
      	cout << "最小生成树权值之和=" << sum;
      }
      
  2. Kruskal算法

    1. 思路

      1. 边集合E中选取权值最小的边,若该边依附的顶点落在 未被访问的顶点集合G 中不同的连通分量上(即:不能形成环)
      2. 否则,舍去此边,选取下一条权值最小的边
      3. 依此类推,直至T中所有顶点都在同一连通分量上为止
    2. 多点孤立,选取最短边;每次考虑是否会构成环

    3. 简单案例

      //边结构,记录边的两个顶点,已经权值
      struct Edge
      {
      	int vex_origin;
      	int vex_finish;
      	int edge_weight;
      };
      //假设结构数组e已经按照权值,从小到大排序
      void Kruskal(Edge e[], int v_num, int e_num) {
      	int i, j, origin, finish, sum = 0;
      	int sn1, sn2;
      	int* vest = new int[v_num + 1];
          //用来记录已经加入最小生成树中的边能到达的最远源点,初始化说明只能到达自身!
      	for (i = 1; i <= v_num; i++) {
      		vest[i] = i;
      	}
      	int k = 1;
      	j = 1;
      	while (k < v_num)//比顶点数小1
      	{
      		origin = e[j].vex_origin;//取出最小边的起点
      		finish = e[j].vex_finish;//最小边的终点
      		sn1 = vest[origin];//作为一个源点
      		sn2 = vest[finish];//选出能到达最远的源点,如finish顶点能到达源点sn2
      		if (sn1 != sn2) {//如果能到达最远的源点相同,不是重合就是回路
      			sum += e[j].edge_weight;
      			k++;
      			for (i = 1; i <= v_num; i++) {
      				if (vest[i] == sn2) {
      					vest[i] = sn1;//更换源点
      				}
      			}
      		}
      		j++;
      	}
      	cout << "最小生成树权值之和=" << sum;
      }
      

排序

1.基本介绍

  1. 稳定与不稳地:如当a=c时,有abc序列,排序后a仍在c前为稳定排序(如bac);否则不稳地排序(如cab)
  2. 排序算法一般 稳定 优先

2.三个O(n^2)的排序

1.插入排序

  1. 数组中遍历每一个元素,其中每个元素a[k]与前面元素作比较;直到比前一个元素大或小或者已到达a[0]

  2. 简单案例(从小到大)

    void insert_sort(int a[],int n){
        for(int i=1; i0 && a[j]

2.冒泡排序

  1. 两两交换(相邻),遍历每个元素,每次范围为[i, n-1]

  2. 可添加标号mark,标记某一次是否已经有序

  3. 简单案例(从小到大)

    void bubble_sort(int a[],int n) {
        for(int i=0; ii!
            int mark=0;
            for(int j=n-1; j>i; j--) {//每次从尾部向上冒泡,最小的一定先出现最上面,所以每次比较范围缩小
                if(a[j]

3.选择排序

  1. 循环,每次选择 非排序的元素中最小的元素 与前面交换

  2. 简单案例(从小到大)

    void select_sort(int a[],int n) {
        for(int i=0; ia[j]) min=j;//下标转移
            }
            swap(a[min],a[i]);
    	}
    }
    

3.希尔排序shell

  1. 思想:将数组分为多个子序列,子序列排序后,再合并

  2. 每个子序列分散在各组中,间隔首先取n/2。如:a[0]与a[2]是一组;a[1]与a[3]是一组…然后间隔继续缩小直到为1!

  3. 注意:不稳定排序,分组时,不同子序列的排序会破环两个相同元素的相对位置!

  4. 时间复杂度:O(n^(3/2))

  5. 简单案例(从小到达)

    void shell_sort(int a[],int n) {
        for(int i=n/2; i>=1; i=i/2) {//间隔不断减少,直到1!!!
            for(int j=0; jfront;1->interval
    void insert_sort(int n, int front, int interval) {
        for(int i=front+interval; ifront && a[j]

4.快速排序【重点】

  1. 思想:选择一个支点,比支点小在左,比支点大在右(从小到大);左右两边继续快排

  2. 时间复杂度:O(nlogn)

  3. 简单案例(从小到达)

    //1、快排支点quickPivot,根据数组的头部和尾部自行确定支点,这里直接取尾部!
    int quickPivot(int l, int r) {
        return r;
    }
    //2、快排调换位置quickSwap
    int quickSwap(int a[], int l, int r) {
        int pivot_index = quickPivot(l, r);//支点索引
        int last = r;//数组最后的一个数据索引
        int pivot = a[pivot_index];
        //将支点放在最后,确保不论根据哪个位置的值进行左右分治,都能连续地从l到r-1
        swap(a[last], a[pivot_index]);
        while (l < r) {
            while (a[l] < pivot) l++;//等于时交换
            while (a[r] >= pivot && r > l) r--;//等于时不交换
            swap(a[l], a[r]);
        }
        //将原本的支点pivot归位,这时l==r!
        //索引比l小的,比a[l]小;索引比l大,比a[l]大;
        swap(a[last], a[l]);
        return l;
    }
    //3、函数quicksort
    void quicksort(int a[], int l, int r) {
        if (r < l) return;
        int pivot = quickSwap(a, l, r);
        //根据参数l和r进行递归
        quicksort(a, l, pivot - 1);//支点左
        quicksort(a, pivot + 1, r);//支点右
    }
    

5.堆排序

  1. 完全二叉树(不存在只有右孩子这种情况!),数组存储

  2. 思想:每次对堆顶去除最小(最大)的元素,并将堆顶元素放在数组最后重新得到新堆顶执行n-1次

  3. 时间复杂度:O(nlogn)

  4. 简单案例(从小到大)

    //建立大顶堆
    void sift_heap(int a[], int parent, int last) {
    	for (int lc = parent * 2 + 1; lc <= last; lc = lc * 2 + 1) {//完全二叉树,左孩子先
    		if (a[lc] < a[lc + 1] && lc + 1 <= last) lc++;//左,右节点最小的一个
    		if (a[parent] < a[lc]) {//父节点比较
    			swap(a[parent], a[lc]);
    			parent = lc;
    		}
    		else break;
    	}
    }
    void heap_sort(int a[], int nums) {
    	for (int node = nums / 2 - 1; node >= 0; node--) {//从最后一个非叶子节点node开始
    		sift_heap(a, node, nums - 1);
    	}
    	//从最后一个元素开始,不停和a[0]进行交换,已经交换的堆积在数组尾部,所以是头不变,尾部不停减1
    	for (int i = nums - 1; i > 0; i--)
    	{
    		swap(a[0], a[i]);
    		sift_heap(a, 0, i - 1);//从根节点index=0,一直往下重新筛选!
    	}
    }
    

6.归并排序

  1. 一般是二分序列,将二分序列合并一个序列,递归合并;当序列长度为1时,返回合并

  2. 时间复杂度:O(nlogn)

  3. 缺点:空间利用多,适用于外排

  4. 优化:二分序列中,可采用一个序列从小到大,另一个序列从大到小,避免比较

  5. 简单案例(从小到大)

    void merge_sort(int a[], int temp[], int left, int right) {
        if (right <= left) return;
        int mid = (left + right) / 2;//二分
        merge_sort(a, temp, left, mid);//左序列
        merge_sort(a, temp, mid + 1, right);//右序列
        //开始左、右序列合并
        for (int i = left; i <= right; i++) temp[i] = a[i];//先复制,从temp比较重新写进a!
        int Lhead = left;//左序列第一个元素索引
        int Rhead = mid + 1;//右序列第一个元素索引
        for (int cur = left; cur <= right; cur++) {
            if (Lhead > mid) a[cur] = temp[Rhead++];//左序列已经访问完,但右序列还有
            else if (Rhead > right) a[cur] = temp[Lhead++];//右序列已经访问完,但左序列还有
            else if (temp[Lhead] > a[Rhead]) a[cur] = temp[Rhead++];
            else a[cur] = temp[Lhead++];
        }
    }
    

7.基数排序

  1. 以0-9作为一个箱子的基准(个,十,百位……)

  2. 思想:

    1. 相继按个、十、百……进行排序。 先让个位有序,再让十位有序,然后百位有序……
    2. 每位上将关键字为k的记录放入第k个箱子
  3. 简单案列(了解)

    //最高有多少位
    int maxBit(int data[], int n) {
    	//先找最大数
    	int max = data[0];
    	for (int i = 1; i < n; i++) {
    		if (max < data[i]) max = data[i];
    	}
    	//数有多少位
    	int bit = 1;
    	while (max >= 10) {
    		max = max / 10;
    		++bit;
    	}
    	return bit;
    }
    //基数排序
    void radixSort(int data[], int n) {
    	int bit = maxBit(data, n);
    	int* temp = new int[n];//辅助存储每一次提出桶后的数据
    	int* count = new int[10];//计数器(桶或箱),无论个十百位,每一位都是0-9!
    	int radix = 1;//记录第几位!!
    	//最高多少位,就要排多少次
    	for (int i = 1; i <= bit; i++) {
    		//清空计数器,每位入桶都不一样
    		for (int j = 0; j < 10; j++) {
    			count[j] = 0;
    		}
    		//统计每个桶中的数据个数
    		for (int j = 0; j < n; j++) {
    			int k = (data[j] / radix) % 10;//每次都取最后一位!!!
    			//radix为1取个位,radix为10取十位
    			count[k]++;
    		}
    		//算出每个数据在temp中的位置,如计数排序中一样
    		//后面的数据出现的位置为前面所有数据出现的次数之和;这样才能对应排序后数组中的位置下标
    		for (int j = 1; j < 10; j++) {
    			count[j] = count[j] + count[j - 1];
    		}
    		//将桶中数据记录到temp中
    		for (int j = n - 1; j >= 0; j--) {
    			int k = (data[j] / radix) % 10;//每次都取最后一位!!!
    			temp[count[k] - 1] = data[j];//注意-1,将次数换为从0开始的下标
    			count[k]--;//次数减一,相同的数值向前移一位
    		}
    		//复制
    		for (int j = 0; j < n; j++) {
    			data[j] = temp[j];
    		}
    		//再下一位
    		radix = radix * 10;
    	}
    }
    

8.计数排序(了解)

  1. 计数排序统计小于等于该元素值的元素的个数i,于是该元素就放在目标数组的索引i位(i≥0)

  2. 基于一个假设,待排序数列的所有数均为整数,且出现在(0,k)的区间之内。如果 k(待排数组的最大值)过大则会引起较大的空间复杂度,一般是用来排序 0 到 100 之间的数字

  3. 简单案例

    struct elemType //每个记录(元素)的结构
    {
    	keyType key;
    };
    struct sqList //顺序表结构
    {
    	elemType r[maxsize + 1]; //下标为0不放元素,从1开始,r[0]一般作为哨兵或缓冲区!
    	int length; //记录元素的个数,不是数组长度
    };
    //计数排序
    void countSort(sqList& l, sqList& after) {
    	//找出最大最小的元素
    	int max = l.r[1].key;
    	int min = l.r[1].key;
    	for (int i = 1; i <= l.length; i++) {
    		if (l.r[i].key > max) max = l.r[i].key;
    		if (l.r[i].key < min) min = l.r[i].key;
    	}
    	const int countLength = max - min + 1;//1号下标存放元素
    	int* count = new int[countLength]; //创建记录出现次数的数组
    	for (int i = 0; i <= countLength - 1; i++) {
    		count[i] = 0;
    	}
    	//统计出现的次数
    	for (int i = 1; i <= l.length; i++) {
    		count[l.r[i].key - min]++; //减去min是下标偏移!
    	}
    	//后面的键值出现的位置为前面所有键值出现的次数之和;这样才能对应排序后数组中的位置下标
    	//比如 1 1 2 2 3;1和2出现两次,所以3再第5个下标
    	for (int i = 1; i <= countLength - 1; i++) {
    		count[i] = count[i] + count[i - 1];
    	}
    	//将数组放到目标位置,倒序!!!
    	for (int i = countLength - 1; i >= 1; i--) {
            //注意自减符号的位置!输出说明次数减一!相同元素往前移一位
    		after.r[count[l.r[i].key - min]--] = l.r[i]; 
    	}
    }
    

你可能感兴趣的:(数据结构,数据结构)