数据结构(三) 排序/并查集/图

目录

1. 排序

2.并查集

3.图


1.排序:

1.1 概念:

排序就是将数据按照某种规则进行排列, 具有某种顺序. 分为内排序和外排序.

内排序就是: 将数据放在内存中的排序; 外排序是: 数据太多无法在内存中排序的.

1.2 插入排序:

插入排序包含: 直接插入排序和希尔排序.

(1) 直接插入排序:

(这里图是借用其他博主的), 直接插入排序就是将第i个数值进行和前i数值依次比较, i数值小就一直放到前面, 直到值比他更小或者比完. 时间复杂度是O(N^2). 稳定性: 稳定.

cpp 复制代码
void InsertSort(int* a, int n)
{
    for(int i = 0; i < n - 1; i++)
    {
        int end = i;
        int tmp = a[end+1];

        while(end >= 0)
        {
            if(tmp < a[end])
            {
                a[end + 1] = a[end];
                end--;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}
(2) 希尔排序:

是采用gap进行分割前后数, 第i个数和i+gap个数进行比较如果a[i+gap]小于a[i]就交换.

gap算一趟, gap每次缩小1/2; 进行每趟调整. 时间复杂度是:O(NlogN); 稳定性: 不稳定.

cpp 复制代码
void ShellSort(int* a, int n)
{
    int gap = n;
    while(gap > 1)
    {
        gap = gap / 2;
        for(int i = 0; i < n - gap; i++)
        {
            int end = i;
            int tmp = a[end + gap];
            while(end >= 0)
            {
                if(tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap; 
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }
}

1.3 选择排序:

选择排序包括选择排序和堆排序:

(1) 选择排序:

每趟找到比最小的数, 遍历全数列的那种, 然后进行交换i和最小数值的位置. 时间复杂度是O(N^2); 稳定性: 不稳定.

还可以依次选两个数, 最大和最小, 放在左边和右边, 进行遍历选择.

cpp 复制代码
void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

void SelectSort(int* a, int n)
{
    for(int i = 0; i < n; i++)
    {
        int start = i;
        int min = start;
        while(start < n)
        {
            if(a[start] < a[min])
                min = start;
            start++;
        }
        Swap(&a[i], &a[min]);
    }
}

void SelectSort(int* a, int n)
{
    int left = 0;
    int right = n - 1;
    while(left < right)
    {
        int minIndex = left;
        int maxIndex = left;

        for(int i = left; i <= right; i++)
        {
            if(a[i] < a[minIndex])
                minIndex = i;
            if(a[i] > a[maxIndex])
                maxIndex = i;
        }

        Swap(&a[minIndex], &a[left]);
        if(left == maxIndex)
        {
            maxIndex = minIndex;
        }
        Swap(&a[maxIndex], &a[right]);
        left++;
        right--;
    }
}
(2) 堆排序:

具体看上一篇博客:数据结构(二)

cpp 复制代码
void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;

    while(child < n)
    {
        if(child + 1 < n && a[child + 1] < a[child])
        {
            child++;
        }
        if(a[child] < a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}


void StackSort(int* a, int n)
{
    for(int i = (n-1-1) / 2; i >= 0; i--)
    {
        AdjustDown(a, n, i);
    }

    int end = n - 1;
    while(end)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        end--;
    }
}

1.4 交换排序:

交换排序包含冒泡排序和快速排序:

(1) 冒泡排序:

相邻两个数进行比较, 大的就交换,这样到最后的就一定是最大的数, 下一次只要遍历到这个最大数前一个即可. 时间复杂度是: O(N^2) ; 稳定性: 稳定 ;

cpp 复制代码
void BubbleSort(int* a, int n)
{
    int end = 0;
    for(end = n - 1; end >= 0; end--)
    {
        int exchange = 0;
        for(int i = 0; i < end; i++)
        {
            if(a[i] > a[i+1])
            {
                Swap(&a[i], &a[i+1]);
                exchange = 1;
            }
        }
        if(exchange = 0)
            break;
    }
}
(2) 快速排序:

时间复杂度就是O(NlogN) , 稳定性: 不稳定.

a.hoare版本

最左边作为key进行比较的值, 其次就是left和right不断往中间走, right找到小于key的, left找到大于key的, 然后交换right和left; 将left和right相遇的点在进行分治法使用快速排序.

cpp 复制代码
void QuickSort1(int* a, int begin, int end)
{
    if(begin >= end)
        return;
    
    int left = begin;
    int right = end;
    int keyi = left;
    while(left < right)
    {
        while(left < right && a[right] >= a[keyi])
        {
            right--;
        }

        while(left < right && a[left] <= a[keyi])
        {
            left++;
        }

        if(left < right)
        {
            Swap(&a[left], &a[right]);
        }
    }
    int meeti = left;
    Swap(&a[keyi], &a[meeti]);

    QuickSort1(a, begin, meeti-1);
    QuickSort1(a,  meeti+1, end);
}
b.挖坑法:

和上面差别就是把key下标的值取出来了, 但是过程还是和上面一样.

cpp 复制代码
void QuickSort2(int* a, int begin, int end)
{
    if(begin >= end)
        return;

    int left = begin;
    int right = end;
    int key = a[left];
    while(left < right)
    {
        while(left < right && a[right] >= key)
        {
            right--;
        } 

        a[left] = a[right];
        
        while(left < right && a[left] <= key)
        {
            left++;
        }
        a[right] = a[left];
    }

    int meeti = left;
    a[meeti] = key;
    QuickSort2(a, begin, meeti - 1);
    QuickSort2(a, meeti + 1, end);
}
c. 前后指针法:
cpp 复制代码
//三数取中;
int GetMidIndex(int* a, int left, int right)
{
    int mid = left + (right - left) / 2;

    if(a[mid] > a[left])
    {
        if(a[mid] < a[right])
            return mid;
        else if(a[left] > a[right])
            return left;
        else
            return right;
    }
}

void QuickSort3(int* a, int begin, int end)
{
    if(begin >= end)
        return;

    int minIndex = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[minIndex]);

    int prev = begin;
    int cur = begin + 1;
    int keyi = begin;
    while(cur <= end)
    {
        if(a[cur] < a[keyi] && ++prev != cur)
        {
            Swap(&a[prev], &a[cur]);
        }
        cur++;
    }

    int meeti = prev;
    Swap(&a[keyi], &a[meeti]);

    QuickSort3(a, begin, meeti-1);
    QuickSort3(a, meeti + 1, end);
}

1.5 归并排序:

归并排序是采用分治的方法, 将数据对半分开, 使用额外的空间进行收集对半开的数组之间的比较大小的数据. 时间复杂度是O(NlogN); 稳定性: 不稳定.

cpp 复制代码
void _MergeSort(int* a, int left, int right, int* tmp)
{
    if(left >= right)
        return;
    
    int mid = left + (right - left) / 2;
    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid+1, right, tmp);
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;

    int i = left;
    while(begin1 <= end1 && begin2 <= end2)
    {
        if(a[begin1] < a[begin2])
            tmp[i++] = a[begin1++];
        else
            tmp[i++] = a[begin2++];
    }

    while(begin1 <= end1)
        tmp[i++] = a[begin1++];
    
    while(begin2 <= end2)
        tmp[i++] = a[begin2++];
    
    for(int j = left; j <= right; j++)
        a[j] = tmp[j];

}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if(tmp == nullptr)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    _MergeSort(a, 0, n - 1, tmp);
    free(tmp);
}

1.6 计数排序:

采用计数每个元素出现的次数, 以及最小值和最大值记录, 利用额外空间记录每个元素出现次数, 然后再将原来数组进行额外数组的替换.

cpp 复制代码
void CountSort(int* a, int n)
{
    int min = a[0];
    int max = a[0];

    for(int i = 0; i < n; i++)
    {
        if(a[i] < min)
            min = a[i];
        if(a[i] > max)
            max = a[i];
    }

    int range = max - min + 1;
    int* count = (int*)calloc(range, sizeof(int));
    if(count == nullptr)
    {
        printf("malloc fail!");
        exit(-1);
    }

    for(int i = 0; i < n; i++)
    {
        count[a[i] - min]++;
    }

    int i = 0;
    for(int j = 0; j < range; j++)
    {
        while(count[j]--)
        {
            a[i++] = j + min;
        }
    }
    free(count);
}

2. 并查集:

2.1 概念:

由于不同元素但是又属于某种集合的数据, 存储使用到并查集合. 元素属于某种集合是按照某种规则来分类的. 需要查询某个元素, 需要找到对应集合去寻找.

下标对应的就是集合编号, 里面的值对应这个元素属于哪个集合里的. 如果值为负数代表这个集合|拥有的元素数目|-1.

2.2 并查集实现:

(1) 并查集结构:

就是采用数组即可.

cpp 复制代码
private:
    vector<int> _ufs;
(2) 初始化并查集:

刚开始每个元素都是-1, 为根结点.

cpp 复制代码
 //初始化并查集:刚开始都是-1.
    UnionFindSet(int n)
        :_ufs(n, -1)
    {}
(3) 查找元素的集合结点:

遍历到负数的结点就是集合结点. 返回下标即可.

cpp 复制代码
//查找元素所在集合:
    int FindRoot(int x)
    {
        int parent = x;
        //遍历到值为负数就是根结点.
        while(_ufs[parent] >= 0)
        {
            //不停迭代下标查询.
            parent = _ufs[parent];
        }
        return parent;
    }

 //递归方法查找;
    int _FindRoot(int x)
    {
        return _ufs[x] < 0 ? x : _FindRoot(_ufs[x]);
    }
(4) 检查两个元素是否在一个集合:

只要检查两个结点是否是同一个集合结点即可.

cpp 复制代码
 //判断两个元素是否在同一个集合中.
    bool InSameSet(int x1, int x2)
    {
        //检查两个元素根结点是否同一个即可.
        return FindRoot(x1)  == FindRoot(x2); 
    }
(5) 两个结点合并:

首先找到两个元素结点的集合结点, 如果在一个集合里面就不用插入了, 不是的话, 将parent1作为元素个数大的集合, parent2进行合并到parent1里面. 然后改变parent1值的个数以及parent2集合的新集合结点.

cpp 复制代码
//合并两个元素所在集合.
    bool UnionSet(int x1, int x2)
    {
        int parent1 = FindRoot(x1); int parent2 = FindRoot(x2);
        if(parent1 == parent2)
            return false;
        
        if(_ufs[parent1] > _ufs[parent2])
        {
            swap(parent1, parent2);
        }

        _ufs[parent1] += _ufs[parent2];
        _ufs[parent2] = parent1;
        return true;
    }
(6) 计算集合个数:
cpp 复制代码
//查询集合里面的个数:
    int GetNum()
    {
        int count = 0;
        for(const int& val : _ufs)
        {
            if(val < 0)
                count++;
        }
        return count;
    }
(7) 压缩路径:

在查找数据的时候就进行压缩路径, 找到该元素的集合结点, 以及它的父结点, 然后进行将这个结点一条路的元素都直接插入到集合结点里面. 而且一般使用于数据量比较大的时候.

cpp 复制代码
//查找元素所在集合:
    //在查找集合结点的时候进行压缩.
    //+压缩路径:
    int FindRoot(int x)
    {
        int root = x;
        //遍历到值为负数就是根结点.
        while(_ufs[root] >= 0)
        {
            //不停迭代下标查询.
            root = _ufs[root];
        }

        while(_ufs[x] >= 0)
        {
            int parent = _ufs[x];
            _ufs[x] = root;
            x = parent;
        }
        return root;
    }

//递归方法查找 + 压缩;
    int _FindRoot(int x)
    {
        //return _ufs[x] < 0 ? x : _FindRoot(_ufs[x]);
        int parent = x;
        if(_ufs[x] >= 0)
        {
            parent = _FindRoot(_ufs[x]);
            _ufs[x] = parent;
        }
    }
(8) 元素编号和用户输入问题:

用户一般不会输入数字编号, 可能会输入关键词, 这时候模板函数解决. 以及使用关键词和集合进行互相关联的方法, 就可以解决了.

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <utility>
#include <unordered_map>
using namespace std;
template<class T>
class UnionFindSet
{
public:
    //初始化并查集:刚开始都是-1.
    UnionFindSet(const vector<T>& v)
        :_ufs(v.size(), -1)
    {
        for (int i = 0; i < v.size(); i++)
        {
            _indexMap[v[i]] = i;
        }
    }

    //查找元素所在集合:
    //在查找集合结点的时候进行压缩.
    //+压缩路径:
    int FindRoot(const T& x)
    {
        int root = _indexMap[x];
        //遍历到值为负数就是根结点.
        while (_ufs[root] >= 0)
        {
            //不停迭代下标查询.
            root = _ufs[root];
        }

        //一般数据量少不需要压缩.
        // while(_ufs[x] >= 0)
        // {
        //     int parent = _ufs[x];
        //     _ufs[x] = root;
        //     x = parent;
        // }
        return root;
    }

    //递归方法查找 + 压缩;
    // int _FindRoot(int x)
    // {
    //     //return _ufs[x] < 0 ? x : _FindRoot(_ufs[x]);
    //     int parent = x;
    //     if(_ufs[x] >= 0)
    //     {
    //         parent = _FindRoot(_ufs[x]);
    //         _ufs[x] = parent;
    //     }
    // }

    //判断两个元素是否在同一个集合中.
    bool InSameSet(const T& x1, const T& x2)
    {
        //检查两个元素根结点是否同一个即可.
        return FindRoot(x1) == FindRoot(x2);
    }

    //合并两个元素所在集合.
    bool UnionSet(const T& x1, const T& x2)
    {
        int parent1 = FindRoot(x1); int parent2 = FindRoot(x2);
        if (parent1 == parent2)
            return false;

        if (_ufs[parent1] > _ufs[parent2])
        {
            swap(parent1, parent2);
        }

        _ufs[parent1] += _ufs[parent2];
        _ufs[parent2] = parent1;
        return true;
    }


    //查询集合里面的个数:
    int GetNum()
    {
        int count = 0;
        for (const int& val : _ufs)
        {
            if (val < 0)
                count++;
        }
        return count;
    }


private:
    vector<int> _ufs;
    //原来标记数据T的处于哪个集合里面.
    unordered_map<T, int> _indexMap;
};

int main() {
    vector<string> v = { "张三", "李四", "王五", "赵六", "田七", "周八", "吴九" };
    UnionFindSet<string> ufs(v);
    cout << ufs.GetNum() << endl; //7

    ufs.UnionSet("张三", "李四");
    ufs.UnionSet("王五", "赵六");
    cout << ufs.GetNum() << endl; //5

    ufs.UnionSet("张三", "赵六");
    cout << ufs.GetNum() << endl; //4

    return 0;
}
相关推荐
王老师青少年编程6 小时前
gesp(C++五级)(12)洛谷:B4051:[GESP202409 五级] 小杨的武器
开发语言·数据结构·c++·算法·gesp·csp
喜-喜6 小时前
Python学习之旅:入门阶段(七)数据结构
数据结构·python·学习
轩源源7 小时前
数据结构——AVL树的实现
开发语言·数据结构·c++·avl树·函数模板·双旋·单旋
Camel卡蒙7 小时前
数据结构——堆(介绍,堆的基本操作、堆排序)
数据结构·算法
冠位观测者7 小时前
【Leetcode 热题 100】45. 跳跃游戏 II
数据结构·算法·leetcode
小禾苗_7 小时前
数据结构——栈
数据结构
记得早睡~8 小时前
leetcode383-赎金信
数据结构·算法·leetcode·哈希算法
gentle_ice8 小时前
leetcode——和为K的子数组(java)
java·数据结构·算法
会蹦的鱼8 小时前
算法6(力扣148)-排序链表
数据结构·leetcode·排序算法