目录

一、二叉搜索树的概念

二、二叉搜索树的中序遍历用于排序+去重

三、二叉搜索树的查找

1、查找的非递归写法

2、查找的递归写法

四、二叉搜索树的插入

1、插入的非递归写法

2、插入的递归写法

五、二叉搜索树的删除

1、删除的非递归写法

2、删除的递归写法

六、二叉搜索树的使用场景

1、key搜索模型(节点存key)

2、key搜索模型整体代码

3、key/value搜索模型(节点既存key又存value)

4、key/value搜索模型整体代码


一、二叉搜索树的概念

二叉搜索树又称二叉排序树。

空树是二叉搜索树,如果一棵树不是空树,需要满足如下情况便可称其为二叉搜索树:

1、左子树上每一个键值均小于根节点;

2、右子树上每一个键值均大于根节点;

3、左右子树均为二叉搜索树。

template <class K>
struct BSTreeNode//用于生成二叉搜索树的节点
{BSTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key;
};
template <class K>
struct BSTree//表示整颗二叉搜索树
{typedef BSTreeNode<K> Node;BSTree():_root(nullptr){}
private:Node* _root;
};

二、二叉搜索树的中序遍历用于排序+去重

通过上面那张图不难发现,用二叉搜索树走个中序,就是升序+去重排序,这也是二叉搜索树又被称为二叉排序树的原因。

使用InOrder调用_InOrder的原因是类外面传参传不了私有的_root,所以采用多套一层的方法。

//中序遍历
void _InOrder(Node* _root)
{if (_root == nullptr){return;}_InOrder(_root->_left);std::cout << _root->_key << " ";_InOrder(_root->_right);
}
void InOrder()//因为外部取不到_root,所以这里套了一层调用函数
{_InOrder(_root);std::cout << std::endl;
}

三、二叉搜索树的查找

对于任意一颗二叉搜索树,最坏的查找次数是数的高度次,时间复杂度O(N)。

如果全国14亿人的身份证号按照完全二叉搜索树进行排列,<14亿<

,也就是说,在14亿人口中,我找到你最坏的情况下仅需要找31次。

可以看到,二叉搜索树针对满二叉树、完全二叉树这种结构平衡的树时,查找效率为O(logN),但是二叉搜索树如果处理有序或接近有序的数据,可能出现上图单子树的情况,大大降低了查找效率,所以它并不是一个很成熟的数据结构,需要平衡二叉树和红黑树对该缺陷进行弥补。(手撕平衡二叉树和红黑树博客尽量一个月左右补上)

1、查找的非递归写法

bool Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else//说明找到了return true;}return false;
}

根据二叉搜索树的性质,左树均小于根,右树均大于根,进行查找。

2、查找的递归写法

Node* _FindR(Node* root,const K& key)
{if (root == nullptr)return nullptr;if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}elsereturn root;
}
bool FindR(const K& key)
{return _FindR(_root, key) == nullptr ? false : true;
}

四、二叉搜索树的插入

二叉搜索树的插入需要考虑插入后,需要维持二叉搜索树的形态。

1、插入的非递归写法

bool Insert(const K& key)
{if (_root == nullptr){_root = new Node(key);//BSTreeNode对象中存放key值,构造一个二叉搜索树节点 }else{Node* parent = nullptr;Node* cur = _root;//cur一直走,走到要插入的位置while (cur){parent = cur;if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else//说明数字重复,插入失败return false;}cur = new Node(key);//判断插入节点放在parent节点的左子树还是右子树if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}}return true;
}

1、如果根是空,插入的节点就是新的根;

2、如果根不为空,就先根据二叉搜索树的性质找到该节点要插入的位置,如果路上遇到相同的数,插入失败;

3、再判断一下,是要插入父亲的左边还是右边即可。

2、插入的递归写法

bool _InsertR(Node*& root, const K& key)//形参是root的引用
{if (root == nullptr){root = new Node(key);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系return true;}if (root->_key < key)return _InsertR(root->_right, key);//看到这个root->_right没,它是下一层root的别名else if (root->_key > key)return _InsertR(root->_left, key);//看到这个root->_left没,它是下一层root的别名else//说明相等,插入失败return false;
}
bool InsertR(const K& key)
{return _InsertR(_root, key);
}

递归写法巧就巧在形参是指针的引用,例如我现在要插入9,下层的root是上一层root->_left的别名, 下层root = new Node(key);即为上一层root->_left=new Node(key);这样插入节点就自动和父节点连接上了。

五、二叉搜索树的删除

二叉搜索树的节点进行删除后,同样需要维持二叉搜索树的形态。

二叉搜索树的删除无非是三种情况:

1、删除的非递归写法

bool Erase(const K& key)
{Node* parent = nullptr;Node* cur = _root;//找到要删除的节点while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else//说明找到要删除的节点了{//开始分析三种情况if (cur->_left == nullptr)//被删除节点左孩子为空。{if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了{_root = _root->_right;}else{if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”parent->_left = cur->_right;elseparent->_right = cur->_right;}    delete cur;}else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空{if (cur == _root){_root = _root->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}    delete cur;}else//被删除节点左右孩子均不为空{//左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根(对被删除节点进行替换)Node* rightMin = cur->_right;//这里选用右树的最小值进行更换Node* rightMinParent = cur;while (rightMin->_left!=nullptr)//因为找最小值,不停找左树即可{rightMinParent = rightMin;rightMin = rightMin->_left;}//std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载//rightMin的左节点必为空,判断父节点的链接方式即可if (rightMinParent->_left == rightMin)//两种情况,第一种如上方图删除8,实际干掉9位置,需要将10的左连至9的右rightMinParent->_left = rightMin->_right;else if (rightMinParent->_right == rightMin)//第二种如上方图删除10,实际干掉14,需要将10的右连至14的右rightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;
}

1、先通过二叉搜索树的性质找到要删除的节点;

2、找到需要删除的节点后,分三种情况进行讨论:

一、被删除节点的左孩子为空,除了cur等于根节点情况下,其他情况下,父节点的孩子指针由指向被删除节点转为指向被删除节点的右孩子。(如图删除9和14)

二、被删除节点的左孩子存在但右孩子为空,除了cur等于根节点情况下,其他情况下,父节点的孩子指针由指向被删除节点转为指向被删除节点的左孩子。(如图删除9)

三、被删除的节点均不为空,可以选用左树最大节点或者右树最小节点对被删除节点进行值替换,问题转化为第一种或第二种情况。(详见代码注释)

2、删除的递归写法

bool _EarseR(Node*& root, const K& key)//形参给了引用,意义同插入的递归写法
{if (root == nullptr){return false;}if (root->_key < key)return _EarseR(root->_right, key);else if (root->_key > key)return _EarseR(root->_left, key);else//说明找到了要删除的节点,无需考虑root的父亲为空{Node* del = root;if (root->_left == nullptr)//被删除节点的左为空root = root->_right;//让root连接root的右树,因为是引用,所以父节点和root是连接的else if (root->_right == nullptr)//被删除节点左不为空但右为空root = root->_left;else//root左右子树均不为空{Node* rightMin = root->_right;while (rightMin->_left!=nullptr)//找到被删除节点的右树最小节点 {rightMin = rightMin->_left;}root->_key = rightMin->_key;//找到了交换key//对子树进行递归删除return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归}delete del;return true;}
}
bool EraseR(const K& key)
{return _EarseR(_root, key);
}

找到节点后,同样需要分三种情况讨论。

1、被删除节点左树为空;

2、被删除节点左树不为空但右树为空;

3、被删除节点左右子树均不为空。

六、二叉搜索树的使用场景

1、key搜索模型(节点存key)

key搜索模型只用key作关键码,结构中只需存key,key即为需要搜索到的值。

例如对英语单词拼写的检查,可以将词库中的所有单词存入二叉搜索树,通过二叉搜索树中检索单词是否存在,达到拼写报错目的。

2、key搜索模型整体代码

template <class K>
struct BSTreeNode
{BSTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key;
};
template <class K>
struct BSTree
{typedef BSTreeNode<K> Node;BSTree():_root(nullptr){}//插入节点bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);//BSTreeNode对象中存放key值 }else{Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else//说明数字重复return false;}cur = new Node(key);//判断插入节点放在parent节点的左子树还是右子树if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}}return true;}bool InsertR(const K& key){return _InsertR(_root, key);}//中序遍历void InOrder()//因为外部取不到_root,所以这里套了一层调用函数{_InOrder(_root);std::cout << std::endl;}//查找bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}elsereturn true;}return false;}bool FindR(const K& key){return _FindR(_root, key) == nullptr ? false : true;}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;//找到要删除的节点while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else//说明找到要删除的节点了{//开始分析三种情况if (cur->_left == nullptr)//被删除节点左孩子为空。{if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了{_root = _root->_right;}else{if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”parent->_left = cur->_right;elseparent->_right = cur->_right;}   delete cur;}else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空{if (cur == _root){_root = _root->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}    delete cur;}else//被删除节点左右孩子均不为空{//左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根Node* rightMin = cur->_right;//这里选用右树的最小值进行更换Node* rightMinParent = cur;while (rightMin->_left!=nullptr){rightMinParent = rightMin;rightMin = rightMin->_left;}//std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载if (rightMinParent->_left == rightMin)//两种情况,第一种如图删除8,实际干掉9位置,需要将10的左连至9的右rightMinParent->_left = rightMin->_right;else if (rightMinParent->_right == rightMin)//第二种如图删除10,实际干掉14,需要将10的右连至14的右rightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;}bool EraseR(const K& key){return _EarseR(_root, key);}
private:Node* _root;void _InOrder(Node* _root){if (_root == nullptr){return;}_InOrder(_root->_left);std::cout << _root->_key << " ";_InOrder(_root->_right);}Node* _FindR(Node* root,const K& key){if (root == nullptr)return nullptr;if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}elsereturn root;}bool _InsertR(Node*& root, const K& key)//形参是root的引用{if (root == nullptr){root = new Node(key);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系return true;}if (root->_key < key)return _InsertR(root->_right, key);else if (root->_key > key)return _InsertR(root->_left, key);elsereturn false;}bool _EarseR(Node*& root, const K& key){if (root == nullptr){return false;}if (root->_key < key)return _EarseR(root->_right, key);else if (root->_key > key)return _EarseR(root->_left, key);else//说明找到了要删除的节点,无需考虑root的父亲为空{Node* del = root;if (root->_left == nullptr)root = root->_right;else if (root->_right == nullptr)root = root->_left;else//root左右子树均不为空{Node* rightMin = root->_right;while (rightMin->_left!=nullptr)//找到右树最小节点 {rightMin = rightMin->_left;}root->_key = rightMin->_key;return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归}delete del;return true;}}
};

3、key/value搜索模型(节点既存key又存value)

key/value搜索模型指每一个key值,都有与之对应的value值,例如英汉互译,一个英文单词可以对应一个翻译字符串。该模型还可以用于统计相同内容出现次数。(举例代码见下方测试函数。)

4、key/value搜索模型整体代码

namespace KV
{template <class K,class V>struct BSTreeNode{BSTreeNode(const K& key,const V& value):_left(nullptr), _right(nullptr), _key(key),_value(value){}BSTreeNode<K,V>* _left;BSTreeNode<K,V>* _right;K _key;V _value;};template <class K,class V>struct BSTree{typedef BSTreeNode<K,V> Node;BSTree():_root(nullptr){}//插入节点bool Insert(const K& key,const V& value){if (_root == nullptr){_root = new Node(key,value);//BSTreeNode对象中存放key值 }else{Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else//说明数字重复return false;}cur = new Node(key, value);//判断插入节点放在parent节点的左子树还是右子树if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}}return true;}bool InsertR(const K& key,const V& value){return _InsertR(_root, key, value);}//中序遍历void InOrder()//因为外部取不到_root,所以这里套了一层调用函数{_InOrder(_root);std::cout << std::endl;}//查找Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}elsereturn cur;}return nullptr;}Node* FindR(const K& key){return _FindR(_root, key);}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;//找到要删除的节点while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else//说明找到要删除的节点了{//开始分析三种情况if (cur->_left == nullptr)//被删除节点左孩子为空。{if (cur == _root)//需要判断cur等于根节点的情况,否则else中parent空指针解引用了{_root = _root->_right;}else{if (parent->_left == cur)//确定cur是parent的左还是右,再进行“托孤”parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;}else if (cur->_right == nullptr)//被删除节点左孩子不为空,右孩子为空{if (cur == _root){_root = _root->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;}else//被删除节点左右孩子均不为空{//左右孩子均不为空,就需要左子树的最大值或右子树的最小值选出来当新根Node* rightMin = cur->_right;//这里选用右树的最小值进行更换Node* rightMinParent = cur;while (rightMin->_left != nullptr){rightMinParent = rightMin;rightMin = rightMin->_left;}//std::swap(cur->_key, rightMin->key);//用std的交换对自定义类型可能比较慢cur->_key = rightMin->_key;//还是用赋值好一点,即使是自定义类型,肯定有写赋值重载cur->_value = rightMin->_value;if (rightMinParent->_left == rightMin)//两种情况,第一种如图删除8,实际干掉9位置,需要将10的左连至9的右rightMinParent->_left = rightMin->_right;else if (rightMinParent->_right == rightMin)//第二种如图删除10,实际干掉14,需要将10的右连至14的右rightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;}bool EraseR(const K& key){return _EarseR(_root, key);}private:Node* _root;void _InOrder(Node* _root){if (_root == nullptr){return;}_InOrder(_root->_left);std::cout << _root->_key << " "<<_root->_value;_InOrder(_root->_right);}Node* _FindR(Node* root, const K& key){if (root == nullptr)return nullptr;if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}elsereturn root;}bool _InsertR(Node*& root, const K& key, const V& value)//形参是root的引用{if (root == nullptr){root = new Node(key,value);//因为root是父节点左/右孩子的别名,直接修改别名,链接关系存在,不用考虑父子节点连接关系return true;}if (root->_key < key)return _InsertR(root->_right, key,value);else if (root->_key > key)return _InsertR(root->_left, key,value);elsereturn false;}bool _EarseR(Node*& root, const K& key){if (root == nullptr){return false;}if (root->_key < key)return _EarseR(root->_right, key);else if (root->_key > key)return _EarseR(root->_left, key);else//说明找到了要删除的节点,无需考虑root的父亲为空{Node* del = root;if (root->_left == nullptr)root = root->_right;else if (root->_right == nullptr)root = root->_left;else//root左右子树均不为空{Node* rightMin = root->_right;while (rightMin->_left != nullptr)//找到右树最小节点 {rightMin = rightMin->_left;}root->_key = rightMin->_key;root->_value = rightMin->_value;return _EarseR(root->_right, rightMin->_key);//return表示子树进行删除,结束掉递归}delete del;return true;}}};
}
void testKV1()//中英互译
{KV::BSTree<std::string, std::string> dic;dic.Insert("data", "数据");dic.Insert("algorithm", "算法");dic.Insert("map", "地图、映射");dic.Insert("Linux", "一款开源免费的操作系统");std::string str;while (std::cin >> str){KV::BSTreeNode<std::string, std::string>* ret = dic.Find(str);if (ret != nullptr){std::cout << "中文翻译:" << ret->_value << std::endl;}elsestd::cout << "查找失败!" << std::endl;}
}
void testKV2()//用于统计次数
{std::string arr[] = { "数学", "语文", "数学", "语文", "数学", "数学", "英语","数学", "英语", "数学", "英语" };KV::BSTree<std::string, int> count;for (auto& e : arr){KV::BSTreeNode<std::string, int>* ret = count.Find(e);if (ret != nullptr){ret->_value++;}else{count.Insert(e,1);}}count.InOrder();
}

【数据结构】二叉搜索树的实现相关推荐

  1. 数据结构---二叉搜索树

    数据结构-二叉搜索树 原理:参考趣学数据结构 代码: 队列代码: #pragma once #define N 100 #define elemType bstTree* #include<st ...

  2. 二叉搜索树的删除操作可以交换吗_JavaScript数据结构 — 二叉搜索树(BST)ES6实现...

    1. 概述 最基本的数据结构是向量和链表,为了将二者的优势结合起来,我们引入了二叉树,可以认为二叉树是列表在维度上的拓展.而今天要介绍的二叉搜索树(BST)则是在形式上借鉴了二叉树,同时也巧妙借鉴了有 ...

  3. 数据结构 二叉搜索树BST的实现与应用

    概念 二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值 ...

  4. [学习][数据结构]二叉搜索树

    定义 一棵二叉搜索树是以一棵二叉树来组织的,如下图.这样一棵树可以使用一个链表数据结构来表示,其中每个节点就是一个对象.除了key和卫星数据之外,每个节点还包含属性left.right和p,他们分别指 ...

  5. 数据结构——二叉搜索树

    一.定义 二叉搜索树(binary search tree),又叫二叉查找树.二叉排序树.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值: 若它的右子树不空,则右子树上所有结点的值均大于 ...

  6. 数据结构——二叉搜索树的C语言实现

    1.什么是二叉搜索树? 2.二叉搜索树的操作 3.二叉搜索树的C语言实现 #include<stdio.h> #include<stdlib.h>#define Element ...

  7. 数据结构 二叉搜索树的删除

    文章目录 概述 待删除的结点没有子树 待删除的结点仅有一颗子树 待删除的结点有两颗子树 C代码实现 概述 这是一篇短文,专门考究一下二叉搜索树的删除. 二叉搜索树的建立非常简单,如果不熟悉的见此文 树 ...

  8. 23王道数据结构二叉搜索树(BST)算法题(6-11题)总结(伪代码)

    6.判断给定的二叉树是否是二叉排序树 算法思想:中序遍历,一棵树为二叉排序树即左右子树为二叉排序树,且当前根节点和左右子树呈递增序列,对左右子树也是如此判断,显然是个递归过程              ...

  9. 【ACM】二叉搜索树(Binary Search Tree /BS Tree) 小结

    动态管理集合的数据结构--二叉搜索树 搜索树是一种可以进行插入,搜索,删除等操作的数据结构,可以用字典或者优先队列. 二叉排序树又称为二叉查找树,他或者为空树,或者是满足如下性质的二叉树. (1)若它 ...

  10. 【LeetCode笔记】96. 不同的二叉搜索树(Java、动态规划)

    文章目录 题目描述 代码 & 思路 精简版 2.0 题目描述 这道题其实不用构造数据结构 二叉搜索树:只要利用这个结构的性质即可,即:左右两子,左小右大 然后用动态规划来做,具体如何推导见思路 ...

最新文章

  1. 时间序列预测实例(prophet的血泪史)
  2. windows获取硬件设备的guid_Windows编程技术:提权技术(下)
  3. HH SaaS电商系统的商城模块设计
  4. opencv实现对象跟踪_如何使用opencv跟踪对象的距离和角度
  5. 【ARTS】01_12_左耳听风-20190128~20190203
  6. 为什么熊掌号没有了_为什么人类总吃食草动物,很少吃食肉动物?
  7. MFC多线程失败:Create Instance failed
  8. java占32位存储空间时,java空间
  9. python入门小程序代码_Python入门小程序(二)
  10. Java 学习多态笔记
  11. 印度大量投资太阳能已取得成效 足以媲美煤炭
  12. Helm 3 完整教程(二十四):创建和使用子 chart
  13. Atitit 遍历文件夹算法 autoit attilax总结
  14. 怎样快速抓取网页中的FLASH动画
  15. Linux Mint 19 Tara Beta 版发布,基于 Ubuntu 18.04
  16. 广州十日 --2006/3/15
  17. R语言使用t.test函数进行t检验、使用配对的t检验(paired)检验组间不独立数据的差异是否有统计学意义
  18. jmap 几个慎用操作
  19. YOLOv5源码逐行超详细注释与解读(3)——训练部分train.py
  20. 微博创作者网址及申请条件,微博创作者收益

热门文章

  1. OpenCV-Python 图像平滑处理2:blur函数及滤波案例
  2. 大语言模型的最新研究方向综述
  3. 日本生活的一些小经验
  4. java家庭理财系统ssm框架
  5. 关于java空指针报错(NullPointException)
  6. 多人对战游戏开发实例之《组队小鸡射击》(附源码)
  7. HTML5新增标签及CSS3新增属性
  8. win11安装node并且配置环境变量
  9. TCP的状态(SYN,FIN,ACK,PSH,RST,URG)
  10. 防不胜防 4K电视和4K屏都有假的!到底怎样才是真4K?