首页 > 其他分享 >超级详细的AVLTree -- 高度平衡二叉树 -- 底层代码实现

超级详细的AVLTree -- 高度平衡二叉树 -- 底层代码实现

时间:2024-09-17 17:24:41浏览次数:12  
标签:Node bf subR cur parent -- AVLTree 二叉树 节点

超级详细的AVLTree – 高度平衡二叉树 – 底层代码实现

目录

  1. AVLTree 简介
  2. 1. 节点结构体定义
  3. 2. AVLTree 类定义及插入函数
  4. 3. 左旋转函数(RotateL)
  5. 4. 右旋转函数(RotateR)
  6. 5. 左右双旋转函数(RotateLR)
  7. 6. 右左双旋转函数(RotateRL)
  8. 7. 中序遍历函数(Inorder)
  9. 8. 计算树的高度(Height)
  10. 9. 判断树是否平衡(IsBalance)
  11. 10.总结
  12. 11.全部代码

提示:文末全部代码更详细,若不想看模块代码可直接划到文末。具体流程图在文中

AVLTree 简介

什么是AVL树?

AVL树(Adelson-Velsky and Landis Tree)是一种自平衡的二叉搜索树(Binary Search Tree, BST)。它通过每个节点的平衡因子(左右子树的高度差)来维护树的平衡,从而确保在最坏情况下的操作复杂度为 O(log n)。AVL树通过旋转操作来调整树的结构,使得插入和删除操作后树始终保持平衡。

二叉搜索树的平衡问题

普通的二叉搜索树在最坏情况下可能退化为一个链表,从而导致查找、插入和删除的时间复杂度退化为 O(n)。为了避免这种情况,AVL树在每次插入或删除节点后都会调整树的结构,确保左右子树的高度差不超过 1。这使得树的高度始终保持在 O(log n) 级别,确保了操作的高效性。

平衡因子

平衡因子是一个节点左右子树高度的差值,定义为:

  • 平衡因子 = 左子树高度 - 右子树高度
  • 平衡因子只能为 -1、0 或 1。若平衡因子超出此范围,则需要进行旋转操作来恢复平衡。

旋转操作

AVL树通过旋转操作来保持平衡。常见的旋转操作包括:

  1. 左旋转(Rotate Left):当右子树高度过高时,进行左旋转。
  2. 右旋转(Rotate Right):当左子树高度过高时,进行右旋转。
  3. 左右双旋(Rotate Left-Right):当左子树的右子树高度过高时,先对左子树进行左旋转,再对父节点进行右旋转。
  4. 右左双旋(Rotate Right-Left):当右子树的左子树高度过高时,先对右子树进行右旋转,再对父节点进行左旋转。

AVL树的优势

  • 快速查找:AVL树通过保持平衡,其高度为 O(log n),因此查找操作的时间复杂度为 O(log n)。
  • 高效的插入和删除:每次插入或删除操作后,通过旋转操作调整树的平衡,插入和删除操作的时间复杂度也是 O(log n)。
  • 适用于动态查找表:AVL树能够自我调整,特别适合频繁插入、删除和查找的场景。

1. 节点结构体定义

// 节点结构体模板
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;  // 键值对
    AVLTreeNode<K, V>* _left;  // 左子节点指针
    AVLTreeNode<K, V>* _right;  // 右子节点指针
    AVLTreeNode<K, V>* _parent;  // 父节点指针

    int _bf;  // 平衡因子 (balance factor)

    // 构造函数,初始化键值对及指针
    AVLTreeNode(const pair<K, V>& kv)
        : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0)
    {}
};

详细解释:

  • 键值对:每个节点存储一个键值对,表示树中保存的数据。
  • 子节点指针:节点有左右子节点指针,用于形成树的结构。
  • 父节点指针:指向该节点的父节点,在旋转操作中用于重新连接树。
  • 平衡因子:表示左右子树的高度差,保持在 -1、0 和 1 之间。

2. AVLTree 类定义及插入函数

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;  // 使用别名简化AVLTreeNode类型的引用

private:
    Node* _root = nullptr;  // 根节点,初始化为空

public:
    // 插入函数,按二叉搜索树的规则插入,并进行旋转调整
    bool insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);  // 如果树为空,则新插入节点为根节点
            return true;
        }

        Node* parent = nullptr;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_kv.first < kv.first)
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (cur->_kv.first > kv.first)
            {
                parent = cur;
                cur = cur->_left;
            }
            else
            {
                return false;  // 键值相同,不能插入重复的键
            }
        }

        cur = new Node(kv);  // 创建新的节点

        if (parent->_kv.first < cur->_kv.first)
        {
            parent->_right = cur;
            cur->_parent = parent;
        }
        else
        {
            parent->_left = cur;
            cur->_parent = parent;
        }

        // 调整平衡因子并检查是否需要旋转
        while (parent)
        {
            if (cur == parent->_left)
            {
                parent->_bf--;  // 左子树增高
            }
            else
            {
                parent->_bf++;  // 右子树增高
            }

            if (parent->_bf == 0)
            {
                break;
            }
            else if (parent->_bf == 1 || parent->_bf == -1)
            {
                cur = parent;
                parent = parent->_parent;
            }
            else if (parent->_bf == 2 || parent->_bf == -2)
            {
                if (parent->_bf == 2 && cur->_bf == 1)
                {
                    RotateL(parent);  // 右右情况,左旋转
                }
                else if (parent->_bf == -2 && cur->_bf == -1)
                {
                    RotateR(parent);  // 左左情况,右旋转
                }
                else if (parent->_bf == -2 && cur->_bf == 1)
                {
                    RotateLR(parent);  // 左右情况,左右双旋
                }
                else if (parent->_bf == 2 && cur->_bf == -1)
                {
                    RotateRL(parent);  // 右左情况,右左双旋
                }
                break;
            }
        }
        return true;
    }
};

详细解释:

  • 插入操作:按照二叉搜索树的规则插入新节点,然后通过回溯更新平衡因子。
  • 旋转操作:插入后,如果平衡因子变为2或-2,则通过旋转恢复平衡。

3. 左旋转函数(RotateL)

左单旋抽象图

void RotateL(Node* parent)
{
    Node* subR = parent->_right;  // 定义parent的右子树
    Node* subRL = subR->_left;    // subR的左子树
    Node* PPNode = parent->_parent;  // parent的父节点

    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;
    parent->_parent = subR;
    subR->_left = parent;

    if (parent == _root)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        subR->_parent = PPNode;
        if (PPNode->_left == parent)
        {
            PPNode->_left = subR;
        }
        else
        {
            PPNode->_right = subR;
        }
    }

    parent->_bf = subR->_bf = 0;
}

详细解释:

  • 旋转过程:将 `parent

节点的右子树subR提升到parent的位置,将parent降为subR` 的左子树。


4. 右旋转函数(RotateR)

右单旋抽象图

void RotateR(Node* parent)
{
    Node* subL = parent->_left;  // 定义parent的左子树
    Node* subLR = subL->_right;  // subL的右子树
    Node* PPNode = parent->_parent;  // parent的父节点

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;
    parent->_parent = subL;
    subL->_right = parent;

    if (parent == _root)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        subL->_parent = PPNode;
        if (PPNode->_left == parent)
        {
            PPNode->_left = subL;
        }
        else
        {
            PPNode->_right = subL;
        }
    }

    parent->_bf = subL->_bf = 0;
}

详细解释:

  • 旋转过程:将 parent 节点的左子树 subL 提升到 parent 的位置,将 parent 降为 subL 的右子树。

5. 左右双旋转函数(RotateLR)

左右双旋
注意这里的折线是指变化的节点的平衡因子呈现-+交替才会被画入折线区域。折线区域代表着双旋转。无论是左右还是右左旋转

void RotateLR(Node* parent)
{
    RotateR(parent->_left);  // 先对左子树进行右旋转
    RotateL(parent);         // 再对parent进行左旋转
}

详细解释:

  • 旋转过程:首先对 parent 的左子树 subL 进行右旋转,将 subL 的右子树上升。然后对 parent 进行左旋转。

6. 右左双旋转函数(RotateRL)

右左双旋

void RotateRL(Node* parent)
{
    RotateL(parent->_right);  // 先对右子树进行左旋转
    RotateR(parent);         // 再对parent进行右旋转
}

详细解释:

  • 旋转过程:首先对 parent 的右子树 subR 进行左旋转,将 subR 的左子树上升。然后对 parent 进行右旋转。

7. 中序遍历函数(Inorder)

void Inorder(Node* node)
{
    if (node)
    {
        Inorder(node->_left);  // 遍历左子树
        cout << node->_kv.first << " ";  // 访问当前节点
        Inorder(node->_right);  // 遍历右子树
    }
}

详细解释:

  • 遍历过程:首先递归遍历左子树,然后访问当前节点,最后递归遍历右子树。

8. 计算树的高度(Height)

int Height(Node* node)
{
    if (node == nullptr)
        return 0;
    int leftHeight = Height(node->_left);
    int rightHeight = Height(node->_right);
    return max(leftHeight, rightHeight) + 1;
}

详细解释:

  • 高度计算:计算左子树和右子树的高度,取较大者再加一作为当前节点的高度。

9. 判断树是否平衡(IsBalance)

bool IsBalance(Node* node)
{
    if (node == nullptr)
        return true;

    int leftHeight = Height(node->_left);
    int rightHeight = Height(node->_right);

    return abs(leftHeight - rightHeight) <= 1 && IsBalance(node->_left) && IsBalance(node->_right);
}

详细解释:

  • 平衡判断:检查左右子树的高度差是否在允许范围内,并递归检查左右子树是否平衡。

总结

AVL树是一种高度自平衡的二叉搜索树,通过旋转操作来维持树的平衡,确保树的高度保持在 O(log n) 级别,从而在最坏情况下也能保持查找、插入和删除操作的高效性。本文详细介绍了 AVL 树的节点结构、插入操作以及旋转操作,并提供了中序遍历、高度计算和树平衡性判断的函数实现。这些操作和算法为 AVL 树的实现提供了坚实的基础,使其能够在实际应用中表现出色。

全部代码

AVLTree.hpp

#pragma once
#include<iostream>
#include<map>
#include<assert.h>
#include<time.h>

using namespace std;

//第一步构建节点结构体
template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;//键值对
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//重点:平衡因子
	int _bf;//balance factor

	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
		//上面这一堆是属于节点的不是属于树的
	{}
};

//接着构建AVLTree
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
private:
	Node* _root = nullptr;//缺省
	
//public:
//
//	AVLTree():_root(nullptr){}
public:
	
	//插入函数
	bool insert(const pair<K, V>& kv)
	{
		//先按搜索二叉树的规则插入
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)//循环到遇到空时break,可以插入
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				//走到这一步说明遇到重复数据,返回false
				return false;
			}
		}

		cur = new Node(kv);//new一个节点出来

		//下面部分是插入节点到树中,不涉及平衡因子的修改
		if (parent->_kv.first < cur->_kv.first)//键值对比大小只能用pair.first
		{
			parent->_right = cur;
			cur->_parent = parent;
			//parent->_bf++;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
			//parent->_bf--;
		}

		//下面部分为循环的修改相关节点的平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}


			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				if (parent->_bf == 2 && cur->_bf == 1)//右右:左单旋
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)//左左:右单旋
				{
					RotateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)//-+左右:左右双旋。下层先做左单旋,最后上层做右单旋
				{
					RotateLR(parent);

				}
				else if (parent->_bf == 2 && cur->_bf == -1)//+-右左:右左双旋.下层先做右单旋,最后上层做左单旋
				{
					RotateRL(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}


		}
		return true;
	};

	//左单旋:在较高的右子树最右侧插入新节点.只能是最右侧
	//只有当parent遇到2的时候才会有左单旋
	void RotateL(Node* parent)
	{
		//首先定义parent右下层为subR,subR的左下层为subRL,parent父节点为ppnode
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* PPNode = parent->_parent;

		//关键过程:subRL成为parent的右孩子。parent成为subR的左孩子。也就是说
		//平衡因子为2的parent高了,我们要把它拉下一层,形成平衡状态
		//具体操作:因为不清楚parent是否是根节点,所以要用ppnode存放。具体后面会提。
		//parent的右孩子指向subRL,subRL的父节点指向parent。parent的父节点指向subR,subR的左孩子指向parent
		//判断一下parent是否为根节点_root,或者parent的父节点是否为空,如果为空,subR的父节点指向空,subR成为新的根节点
		//如果不为空,判断一下原来parent是ppnode的左孩子还是右孩子,然后用ppnode的左右指针指向subR,subR的父节点指向ppnode
		//最后旋转完成的部分(parent+subR)的平衡因子都会变成0
		parent->_right = subRL;
		if(subRL)
		subRL->_parent = parent;
		parent->_parent = subR;
		subR->_left = parent;

		if (parent == _root)
		{
			subR->_parent = nullptr;
			_root = subR;
		}
		else
		{
			subR->_parent = PPNode;
			if (PPNode->_kv.first < parent->_kv.first)
			{
				PPNode->_right = subR;
			}
			else
			{
				PPNode->_left = subR;

			}
			
		}
		parent->_bf = subR->_bf = 0;
	}

	// 右单旋:在较高的左子树最左侧插入新节点时需要右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppNode = parent->_parent;

		// 关键过程:subLR成为parent的左孩子,parent成为subL的右孩子。
	    // 平衡因子为-2的parent高度过高,我们要把它拉下一层,恢复平衡。
	    // parent的左孩子指向subLR,subLR的父节点指向parent。subL的右孩子指向parent,subL成为新的父节点。

		parent->_left = subLR;
		//subLR->_parent = parent;
		if (subLR)//如果subLR为空则其没有父节点
			subLR->_parent = parent;
		subL->_right = parent;
		parent->_parent = subL;

		// 判断parent是否为根节点_root。如果parent是根节点,subL成为新的根节点。
	    // 如果parent有父节点ppNode,判断parent是ppNode的左孩子还是右孩子,并更新ppNode的指针指向subL。

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			subL->_parent = ppNode;
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
		}

		parent->_bf = subL->_bf = 0;
	}

	// 左右双旋:在较高的左子树的右侧插入新节点时,需要先对左子树(parent->_left)进行左旋,再对父节点(parent)进行右旋.最后以parent->_left->_right为根节点
	void RotateLR( Node* parent)//右双旋
	{
		// subL是parent的左子树,subLR是subL的右子树
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//我们不清楚新节点插入到哪里,所以用bf来标注。 用于后续调整平衡因子
		int bf = subLR->_bf;
		RotateL(subL);// 左右双旋的第一步是先对subL进行左单旋
		RotateR(parent);// 然后对parent进行右单旋

		// 根据subLR的平衡因子调整旋转后子树的平衡因子
		if (bf == -1)
		{
			subL->_bf = 0;
			parent->_bf = 1;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			parent->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}


	}

	// 右左双旋:在较高的右子树的左侧插入新节点时,需要先对右子树进行右旋,再对父节点进行左旋
	void RotateRL(Node* parent) // 右左双旋
	{
		// subR是parent的右子树,subRL是subR的左子树
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;// 用于后续调整平衡因子

		RotateR(subR);// 右左双旋的第一步是先对subR进行右单旋
		RotateL(parent);  // 然后对parent进行左单旋

		if (bf == -1) // subRL左子树新增
		{
			subR->_bf = 1;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1) // subRL右子树新增
		{
			subR->_bf = 0;
			parent->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == 0) // subRL自己就是新增
		{
			subR->_bf = 0;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	//这里为什么要写2个中序遍历呢?
	//AN:实例化对象不能够调用类私有的成员变量如果直接在类外写:tree.inorder(Node* root)这个会报错。
	//我们只能够通过公有化接口去调用inorder函数,也就是实例化对象后外函数调内函数,内函数调私有变量。
	//下面的求高度,是否平衡也是这个逻辑
	void Inorder()
	{
		_Inorder(_root);
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	//通过递归算法求二叉树的高度
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}

	//通过递归算法求二叉树是否平衡
	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
		//abs求绝对值
		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}
};


AVLTree.cpp

#include"AVLTree.hpp"
void TestAVLTree1()
{
    //3组数据建议全部测试一遍
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.insert(make_pair(e, e));
	}

	t.Inorder();

	cout << t.IsBalance() << endl;
}

void TestAVLTree2()
{
	srand(time(0));
	const size_t N = 100000;
	AVLTree<int, int> t;
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand();
		t.insert(make_pair(x, x));
		//cout << t.IsBalance() << endl;
	}

	//t.Inorder();

	cout << t.IsBalance() << endl;
}

int main()
{
	TestAVLTree1();
	//TestAVLTree2();
	return 0;

}

标签:Node,bf,subR,cur,parent,--,AVLTree,二叉树,节点
From: https://blog.csdn.net/youchou1274/article/details/142316179

相关文章

  • 【python学习】深入掌握 Python RQ 任务队列库:全面处理异步任务的实战指南
    引言rq是基于Redis的Python任务队列库,用于处理异步任务。它能帮助开发者将繁重的后台任务交由独立进程执行,从而提高系统性能。在复杂项目中,任务的超时、重试、定时执行、依赖关系以及队列优先级等功能尤为重要。本文将全面介绍rq的常用和高级功能,帮助你在项目中灵活......
  • 素数个数[中秋快乐~]
    题目描述编程求 2 ~ n (包括 n)中有多少个素数。输入格式输入 n(2≤n≤50000)。输出格式素数个数。输入数据110 输出数据14代码#include<bits/stdc++.h>usingnamespacestd;intmain(){intn,ans=0;cin>>n;for(inti=2;i<=n;i++){......
  • 我使用本地windows11上的VSCode远程连接到ubuntu进行RUST程序开发,我在VSCode上安装了
    当你使用VSCode的Remote-SSH扩展从本地Windows11连接到远程的Ubuntu服务器进行开发时,插件的安装有以下行为:插件的安装位置本地插件:某些插件,例如VSCode的界面插件或与本地编辑器相关的插件,安装在你的本地Windows系统上。这些插件不需要与远程服务器交互,因此它们仅......
  • 过拟合与欠拟合、批量标准化
    过拟合与欠拟合过拟合(Overfitting)1、基本概念:过拟合指的是模型在训练数据上表现很好,但在未见过的测试数据上表现较差的情况。过拟合发生的原因是模型过于复杂,能够记住训练数据的细节和噪声,而不是学习数据的通用模式。2、特征:模型在训练数据上的准确度高。模型在测试数据......
  • 微信授权登录接口开发
    微信登陆过程在项目开发中,难免会遇到微信授权登录这一操作,本讲来讲一下微信登陆是如何实现的?关于校验登录,有诸多方法,记录方法如下:使用SpringMVC提供的拦截器网关服务全局过滤器使用AOP面向横切面实现对于使用SpringMVC提供的拦截器来实现,其大致的思路如下:注意:用户......
  • 网络编程速递小项目
    一.项目需求功能描述:仿照顺丰速运功能,实现一个自己的速递项目。自行定义项目名,最终可以提现到简历中服务器:处理客户端的请求,并将数据存入数据库中,客户端请求的数据从数据库进行获取,服务器转发给客户端。用户客户端:实现账号的注册、登录、注销、修改用户密码功能,当登录成......
  • STM32F407单片机编程入门(三)ADC详解及实战含源码
    文章目录一.概要二.STM32F407VET6单片机ADC外设特点三.STM32单片机ADC内部结构图1.ADC相关引脚说明2.ADC通道分类3.触发源4.转换周期5.电压转换计算6.更精确电压转换计算四.规则通道ADC采集信号流向1.单次转换模式2.连续转换模式五.CubeMX配置一个ADC采集例程六.CubeMX......
  • 2024年全国大学生数学建模竞赛B题生产过程中的决策问题分析
    目录引言问题1:抽样检测方案设计问题2:生产过程中的决策决策阶段划分决策方案结果问题3:多道工序和零配件的决策生产流程决策过程问题4:基于抽样检测的重新决策动态调整次品率结论引言在2024年全国大学生数学建模竞赛B题中,企业需要对生产中的各个环节进行决策......
  • C#外部调用bartender条码
    送bartender2016企业版,功能完整,能外部调用一维码,二维码,打印张数以及其他信息,引用seagull.bartender.print.dll文件调用打印            engine.Start();            stringluj="D:\打印模板\打印测试.btw";     ......
  • 数据生态进化论:仓库、中台、飞轮引领企业变革
    前言企业对于数据的运用以及大数据的发展都和互联网的发展息息相关,每一次重大的技术跃迁都是为了解决积累已久的各种问题。本文将从互联网进程的角度来分析数据仓库到数据中台、再到数据飞轮的历程。数据仓库,互联网快速发展时代背景数据仓库的概念最早由比尔·恩门(BillInmon)于1990......