败者树构建与调整详解(含测试代码)

 前言

此文仅记录学习过程,详细讲解了败者树的构建过程以及构建完毕之后的调整。

背景:在leetcode刷题时,遇到23.合并K个升序链表。第一时间就想到了外部排序用到的败者树,但是其数据结构让我一时半会难以想到如何构造,看来如果只是看一遍算法的话还是无法深刻理解其本质。(虽然可以用优先队列的小根堆可以做题目来,但是学习不是固地自封)

反省:学习途中遇到的算法应该自己实现一遍,知道其中核心细节以及其主要用途,转化为自己的知识。

败者树的运作过程

败者树其实是树形选择排序的一种变形,其是一个完全二叉树。顾名思义败者树中的非叶子节点保留的是败者,ls[0]记录的是最终胜者,也就是没有被保留在败者节点的人。

算法:就是从叶子节点出发,假设一开始的叶子节点是胜利者,与父节点比较

下图就是一个5路-归并排序败者树数据结构例子:

bi是一个抽象数据结构,其数据有序排列。第一个数据被读取后将会使得第二个数据为第一个数据,可以用栈实现,也可以用数组加指针。

栈:pop第一个数据,第二数据就可以变成第一个

顺序表加指针:第一个数据被读取后,指针向后移动。数组与链表都可以这样实现。

int ls[5];  //ls[t]记录的数据如果是i,bi的第一个元素曾经在这个节点比赛被打败。

不纠结如何实现,看一下算法核心应该是运作的。

显示每个bi(i=0~4)的第一个元素参与败者树。顺序也可以逆序也可以,我们选择逆序从b[4]开始参与败者树的调整。

败者树构建运作过程(规定小元素获胜):

一开始还未比赛的时候,b4查看ls[4],发现没有对手(可以设置特殊值-1),那就必须留在原地,等待对手,不然败者树就无法构成,此时ls[4]=4。

b3参与败者树,查看ls[4]=4,知道对手是b4的第一个元素,那么就开始比较。b3的第一个元素6小于b4的第一个元素12,所以ls[4]记录4。b3继续向上比较,查看ls[2],发现没有对手,那么就留在原地,ls[2]=3。

b2参与败者树,查看ls[3],发现没有对手,那就必须留在原地,等待对手,此时ls[3]=2。

b1参与败者树,查看ls[3],发现ls[3]=2,与b2的第一个元素进行比较,b1的第一个元素9小于b2的第一个元素20,那么ls[3]=2。b1继续向上比较,查看ls[1],发现没有对手,那么就留在原地,ls[1]=1。

b0参与败者树,查看ls[2],发现ls[2]=3,与b3第一个元素进行比较,b3的第一个元素6小于b0的第一个元素10,所以ls[2]=0,让之前ls[2]记录的3向上继续比较,那么b3查看ls[1],发现ls[1]=1,与b1第一个元素进行比较,b3的6<b1的9,所以ls[1]=1,ls[0]=3。

显然胜者为b3的第一个元素6。且败者树的每个节点(ls[i](i=1~4)都有败者的编号记录在上面。

比赛至多在ls[1]上比赛,然后决出最终胜者。

构建完成后的败者树调整

取出胜者,ls[0]编号的b中的第一个元素被取出,那么就将b中下一个元素变成第一个元素参与败者树的调整,与上方调整一样,不过要分类讨论。

1.ls[0]编号的b有下一个元素

那么直接就让下一个元素变成第一个元素参与败者树。

2.ls[0]编号的b没有下一个元素

那么就添加一个元素使得这个元素为无穷大inf,因为小的胜利所以如果b中没有元素了,那么其父节点的比赛就直接让等待的人向上比赛。

比如上图中ls[0]就是3编号的b,b3如果只有一个元素6,下一个元素没有,那么添加一个元素无穷大,与父节点比较的时候,b3肯定会输,所以ls[4]=3,而原先的ls[4]=4编号的b向上比赛。所以,在b4来到ls[4]这个节点比赛的时候,肯定会赢而向上比赛。

败者树代码

curPoint定位到第一个元素。

lists里面存放的是一条条有序的链表。

#include<iostream>
#include<string>
#include<vector>

using namespace std;

struct ListNode{
	int val;
	ListNode* next;
	ListNode() :val(0), next(nullptr) {};
	ListNode(int x) :val(x), next(nullptr) {};
	ListNode(int x, ListNode* p) :val(x), next(p) {};
};


//分析败者树数据结构
//输入的是链表数组,每个链表相当于归并块

//败者树上记录数组下标以表示败者的归并块在链表数组中的位置,只需要记录下标即可。

//当胜者被读取后,需要将胜者节点从链表中弹出,定位到下一个节点,
//由于链表只能前向迭代,所以将记录指针定位到下一个节点即可。

const int k = 5;//5路-归并排序, 多少有序的线性表多少路归并 ,可自定义或者根据lists中有多少归并块设置。

vector<int> my_ls(k);//非叶子节点除根节点,记录败者归并块位置,根节点记录胜者。

vector<ListNode*> curPoint(k);//记录与数组下标相同的归并块(链表)的当前元素,当胜者节点被读取时,需要将curPoint数组对应指针向后迭代。

//存储归并块(链表)的数组。
//vector<ListNode*> lists(k,new ListNode());//错误,初始化调用fill()引用了new ListNode()指针,在赋值给数组中的所有指针,指针数组都定位到同一个内存地址
vector<ListNode*> lists;


ListNode* K_Merge(vector<int>& ls, vector<ListNode*>& curPoint);
void init(const vector<ListNode*>& lists);
void adjust(vector<int>& ls, vector<ListNode*> curPoint, int i);


void init(const vector<ListNode*> &lists)
{
	curPoint.resize(k, nullptr);

	for (int i = 0; i < k; i++)
	{
		my_ls[i] = k;//没有k编号的lists,这里表示没有人在比赛
	}

	for (int i = 0; i < k; i++)
	{
		curPoint[i] = lists[i];
	}

	for (int i = 0; i < k; i++)
	{
		adjust(my_ls, curPoint, i);
	}
}

void adjust(vector<int>& ls, vector<ListNode*> curPoint, int i)
{

	int t = (i + k) / 2;

	while (t > 0)
	{

		if (i!=k && (ls[t]==k || curPoint[i]->val > curPoint[ls[t]]->val))
		{
			int temp = i;
			i=ls[t];
			ls[t] = temp;
		}
		t /= 2;
	}
	ls[0] = i;
}

ListNode* K_Merge(vector<int>& ls, vector<ListNode*>& curPoint)
{
	ListNode head, * tail = &head;
	ListNode* maxNode = new ListNode(INT_MAX);
	while (curPoint[ls[0]]->val != INT_MAX)
	{
		tail->next = curPoint[ls[0]];
		tail = tail->next;

		curPoint[ls[0]] = curPoint[ls[0]]->next;
		if (curPoint[ls[0]] == nullptr)
		{
			curPoint[ls[0]] = maxNode;
		}

		adjust(ls, curPoint, ls[0]);
	}
	return head.next;

}

int main()
{
	for (int i = 0; i < k; i++)
	{
		lists.push_back(new ListNode());
	}

	for (int i = 0; i < k; i++)
	{
		ListNode* tmp = lists[i];
		for (int j = 0; j < k; j++)
		{
			tmp->val = i + j;
			if (j == k - 1) break;
			ListNode* p = new ListNode();
			tmp->next = p;
			tmp = tmp->next;
		}
	}
	init(lists);
	ListNode* ans = K_Merge(my_ls, curPoint);
	ListNode* tmp = ans;
	while (tmp != nullptr)
	{
		cout << tmp->val << " ";
		tmp = tmp->next;
	}

	system("pause");
	return 0;
}
//
//0 1 1 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 7 7 8

败者树用途

主要用于大型文件在小内存的情况下如何进行排序。

我们一般的排序算法都是在小数据下进行的排序,将数据放在内存上就基本足够了,但是数据如果大于内存的话,那么就需要将数据一部分一部分放到内存进行排序。

例如上文有5个b,那么等量的取得每个b中前部分数据放入内存,然后进行败者树的建立与调整。

当存放bi的前部分数据全部参与败者树的时候,就会回到磁盘看是否bi部分是否还有数据,如果有就读入到内存中存放bi的位置。例如刚开始,内存分配了一个b的容量与一个物理块(称wx),那么每个bi读入1/5的b的容量(称1/5bi的数据放在xi的内存区),然后进行败者树的构建与调整,胜者放到空的物理块中。如果某个xi中的数据为空,那么就回去磁盘上找bi看还有没有数据,有就继续写入min(1/5bi容量,bi真实容量)。

当数据排序好的数据足够一个物理块的时候,将其写回到磁盘。

微观上内存内在进行败者树构建与调整,其实宏观上内存与磁盘也在进行一种败者树的构建与调整。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值