啊哈算法(7)——神奇的树

1、堆(完全二叉堆)

  • 建堆的方法:(自上而下的向上调整)每一次插入一个新节点,并且对其进行向上调整,实现如下:
/*建立最小堆,方法一*/

#include<iostream>

using namespace std;

int h[101];        //对于n个节点的堆

void swap(int a, int b)
{
    int temp = h[a];
    h[a] = h[b];
    h[b] = temp;
}

void siftup(int i)    //向上调整
{
    int flag = 0;
    if (i == 1) return;// 如果是堆顶直接退出

    //当不在堆顶的时候,并且当前节点i的值比其父节点要小的时候就向上调整
    while(i!=1&&flag==0)
    {
        //判断它与其父节点的大小
        if (h[i] < h[i / 2])
            swap(i, i / 2);
        else
            flag = 1;  //表示已经不需要向上调整了
        i = i / 2; //更新节点的编号,方便下一次继续向上调整
    }
}

int main()
{
    int n;//节点个数 这里最多100
    cin >> n;
    //建立一个最小堆
    for (int i = 1; i <= n; ++i)
    {
        cin >> h[i];
        siftup(i);
    }
    for (int i = 1; i <= n; ++i)
    {
        cout << h[i] << " ";
    }
    system("pause");
}

上述方法建堆的整体时间复杂度为O(NlogN)。

  • 建堆的方法二:(自下而上的向下调整)在一个完全二叉树中,如果所有的子树都符合最小堆的特性,那么整棵树就是一个完全二叉堆了。我们从最后一个非叶节点开始,即n/2。依次对每一个节点实行向下调整即可,具体的实现如下:
/*建立最小堆,方法一*/

#include<iostream>

using namespace std;

int h[101];        //对于n个节点的堆
int n;            //节点个数

void swap(int a, int b)
{
    int temp = h[a];
    h[a] = h[b];
    h[b] = temp;
}

void siftdown(int i) //传入需要向下调整的节点编号
{
    int t, flag = 0;//flag用来标记是否需要继续向下调整

    //当i节点有儿子(至少有左儿子) 并且需要向下调整就循环执行
    while (2*i<=n&&flag==0)
    {
        //首先判断它和左孩子的关系
        if (h[i] > h[i * 2])
            t = 2 * i;
        else
            t = i;

        //如果有右儿子在对右儿子进行讨论
        if (2 * i + 1 <= n)
        {
            //如果右儿子更小,更新最小节点的编号
            if (h[t] > h[i * 2 + 1])
                t = i * 2 + 1;
        }

        //如果发现需要向下调整
        if (t != i)
        {
            swap(t, i);
            i = t;         //更新需要向下调整的节点的编号,方便下一次进行调整
        }
        else
            flag = 1;     //若想等说明不需要调整了
    }
}

void create()
{
    for (int i = n / 2; i >= 1; --i)
        siftdown(i);                     //从最后一个非叶节点开始进行向下调整
}


int main()
{

    cin >> n;
    //建立一个最小堆
    for (int i = 1; i <= n; ++i)
    {
        cin >> h[i];
    }

    create();
    for (int i = 1; i <= n; ++i)
    {
        cout << h[i] << " ";
    }
    system("pause");
}

用这种方法来建立一个堆的时间复杂度为O(N)。

2、堆排序
堆的一个作用就是进行堆排序,和快排的时间复杂度一样为O(NlogN)。堆排序的实现简单,可以先建立一个最小堆,每一次删除堆顶的元素将其加入到一个数组中,直到堆空为止。实现代码如下:


/*建立最小堆,堆排序方法一*/

#include<iostream>

using namespace std;

int h[101];        //对于n个节点的堆
int n;            //节点个数

void swap(int a, int b)
{
    int temp = h[a];
    h[a] = h[b];
    h[b] = temp;
}

void siftdown(int i) //传入需要向下调整的节点编号
{
    int t, flag = 0;//flag用来标记是否需要继续向下调整

    //当i节点有儿子(至少有左儿子) 并且需要向下调整就循环执行
    while (2*i<=n&&flag==0)
    {
        //首先判断它和左孩子的关系
        if (h[i] > h[i * 2])
            t = 2 * i;
        else
            t = i;

        //如果有右儿子在对右儿子进行讨论
        if (2 * i + 1 <= n)
        {
            //如果右儿子更小,更新最小节点的编号
            if (h[t] > h[i * 2 + 1])
                t = i * 2 + 1;
        }

        //如果发现需要向下调整
        if (t != i)
        {
            swap(t, i);
            i = t;         //更新需要向下调整的节点的编号,方便下一次进行调整
        }
        else
            flag = 1;     //若想等说明不需要调整了
    }
}

void create()
{
    for (int i = n / 2; i >= 1; --i)
        siftdown(i);                     //从最后一个非叶节点开始进行向下调整
}

int deletemax()
{
    int t;
    t = h[1]; //用一个变量记录堆顶的元素
    h[1] = h[n];// 将最后一个元素赋给堆顶
    n--;//堆的规模减少1;
    siftdown(1);//对堆顶元素进行向下调整
    return t;
}

int main()
{

    cin >> n;
    //建立一个最小堆
    for (int i = 1; i <= n; ++i)
    {
        cin >> h[i];
    }

    create();

    int num = n;
    //删除顶部元素 连续删除n次;
    for (int i = 1; i <= num; ++i)
    {
        cout << deletemax() << " ";
    }
    system("pause");
}

此外堆排序还有一种更好的实现方法,是建立最大堆而不是最小堆。建立好最大堆之后最大的元素在h[1]的位置,每一次只是需要将h[1]和h[n]的位置互换,这样最大元素h[n]已经归为,并且在对h[1]进行一次向下调整,让其满足堆序性。实现如下:

/*建立最大堆,堆排序方法二*/

#include<iostream>

using namespace std;

int h[101];        //对于n个节点的堆
int n;            //节点个数

void swap(int a, int b)
{
    int temp = h[a];
    h[a] = h[b];
    h[b] = temp;
}

void siftdown(int i) //传入需要向下调整的节点编号
{
    int t, flag = 0;//flag用来标记是否需要继续向下调整

    //当i节点有儿子(至少有左儿子) 并且需要向下调整就循环执行
    while (2*i<=n&&flag==0)
    {
        //首先判断它和左孩子的关系
        if (h[i] < h[i * 2])
            t = 2 * i;
        else
            t = i;

        //如果有右儿子在对右儿子进行讨论
        if (2 * i + 1 <= n)
        {
            //如果右儿子更小,更新最小节点的编号
            if (h[t] < h[i * 2 + 1])
                t = i * 2 + 1;
        }

        //如果发现需要向下调整
        if (t != i)
        {
            swap(t, i);
            i = t;         //更新需要向下调整的节点的编号,方便下一次进行调整
        }
        else
            flag = 1;     //若想等说明不需要调整了
    }
}

void create()
{
    for (int i = n / 2; i >= 1; --i)
        siftdown(i);                     //从最后一个非叶节点开始进行向下调整
}

void heapsort()
{
    while (n>1)
    {
        swap(1, n);
        n--;
        siftdown(1);
    }
}

int main()
{

    cin >> n;
    //建立一个最大堆
    for (int i = 1; i <= n; ++i)
    {
        cin >> h[i];
    }

    create();

    int num = n;
    heapsort();

    //输出结果
    for (int i = 1; i <= num; ++i)
    {
        cout <<h[i]<< " ";
    }
    system("pause");
}

3、树的应用——并查集算法
对于如下输入,第一行n,m,n表示强盗的人数,m表示警方搜集到的m条线索。接下来的m行每一行有两个数a b,表示强盗a和强盗b是同伙,现在要求有多少个独立的犯罪团伙:

10 9
1 2
3 4
5 2
4 6
2 6
8 7
9 7
1 6
2 4

通过并查集算法来实现。并查集通过一个一维数组来实现,本质是维护一个森林。刚开始的时候,森林的每一个点都是孤立的,之后通过一些条件,逐渐将这些树合并成一个较大的树。合并的过程遵循靠左原则和擒贼先擒王原则,在每一次判断两个点是否已经在同一棵树中(一棵树其实就是一个集合),也要注意必须寻求其根源,中间的父节点不能说明问题,必须找到其祖宗(树的根节点),通过判断两个根节点是否属于同一个集合才可以,对于上例实现如下:

/*并查集算法应用*/

#include<iostream>

using namespace std;

int f[1001];    //最多输入1000个节点
int n, m;//节点个数和线索数目

void init()   //刚开始每个节点都是孤立的
{
    for (int i = 1; i <= n; i++)
    {
        f[i] = i;
    }
}

//寻找祖宗的过程
int getf(int v)
{
    if (f[v] == v)
        return v;
    else
    {
        //向上继续寻找其祖宗,并且在递归函数返回的时候,把中间的父节点都改为最终找到的祖宗的编号
        //这其实就是路径压缩,可以提高以后找到最高祖宗的速度
        f[v] = getf(f[v]);
        return f[v];
    }
}

//合并两个子集合
void merge(int v, int u)
{
    int t1 = getf(v);
    int t2 = getf(u);
    if (t1 != t2)
    {
        f[t2] = t1;// 遵循靠左合并的原则
    }
}

int main()
{
    int sum = 0;
    cin >> n >> m;

    //初始化
    init();

    int v, u;
    for (int i = 1; i <= m; ++i)
    {
        cin >> v >> u;
        merge(v, u);
    }

    for (int i = 1; i <= n; ++i)
    {
        if (f[i] == i)
            sum++;                 //统计有多少个独立的集合
    }
    cout << sum;
    system("pause");
}

猜你喜欢

转载自blog.csdn.net/xc13212777631/article/details/82078908
今日推荐