1、堆(完全二叉堆)
/*建立最小堆,方法一*/
#include
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)。
/*建立最小堆,方法一*/
#include
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
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
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 <" ";
}
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
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");
}