Splay-Tree理解
简介
splay tree其实就是不停的旋转,没进行一个操作都要进行旋转;例如,当访问某一个结点的时候,会通过旋转其结点使得该结点变为树根,这样保证其的平均复杂度为O(nlogn);
其的操作包括:
查找:如果查找成功(找到的话),那么由于伸展操作,被查找的结点成为新的树根;但是如果查找失败的话(没有找到),那么在查找遇到NULL之前的那个结点将会成为新的根,也就是距离要查找的值的最靠近的值的结点;
插入:根据BST的性质进行插入到相应的位置,接着执行伸展,将该值旋转到根结点上;
查找最大最小:利用BST的性质查找,查找之后要伸展,将查找的值伸展到根上;
删除:先将要删除的结点伸展至根上,将左子树上最大的结点伸展至左子树的根结点(保证右子树为空),再将右子树连接到左子树的右结点上,删除跟结点,此时左子树的根结点为根;
合并:将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1 中最大的一个元素x,再通过Splay(x,S1)将x 调整到伸展树S1 的根。然后再将S2 作为x 节点的右子树。这样,就得到了新的伸展树S。如图:
分离:以x 为界,将伸展树S 分离为两棵伸展树S1 和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x 调整为伸展树的根节点,则x 的左子树就是S1,而右子树为S2。如图:
注意
分为自顶向下和自底向上,自底向上需要关注到每个结点的左右子结点还有父亲结点和祖父结点,但是自顶向下则只需要关注左右子结点;
下面将会有自顶向下和自底向上两种实现方法:
原理
伸展树的重点在于伸展,伸展一共有六种旋转,分别是左单旋转,右单旋转,左左旋转,右右旋转,左右旋转,右左旋转,后四种旋转均可以通过前面两种旋转实现;
1.zig/zag(右单旋转/左单旋转)
我们发现
当结点x是结点y的左子结点时,我们对结点x进行一次右旋,使得结点x是结点y的根结点;
当结点y是结点x的右子结点时,我们对结点y进行一次左旋,使得结点y为结点x的根结点;
需要注意的是,上面的情况是自顶向下的,也就是会对子结点进行旋转,但是如果是自底向上的话,则会对父结点进行旋转;
2.zigzag/zagzig(右左旋转/左右旋转)
这个旋转也称为双旋转;
如图
当结点x的父结点不为根结点,同时x为父结点y的左子结点,y为z的右子结点,则称为右左旋转,先对结点x进行一次右旋转,此时y结点为x结点的右子结点,这时三个结点连成一条线,接着对父结点进行一次左旋转,使得x结点为根结点;
相反操作则称为左右旋转;
3.zigzig/zagzag(左左旋转/右右旋转)
上面两种情况均是之前AVL树中就存在的,但是这个情况是Splay树中才存在的;
当x结点和y结点均为其父结点的左子结点时,则称为zagzag,此时先右旋转y结点,再右旋转x结点;
相反则称为右右旋转,也就是zigzig
实现
自顶向下
//
// main.cpp
// splay
//
// Created by staff on 16/10/28.
// Copyright © 2016年 George1994. All rights reserved.
//
#include <iostream>
/*
* C++ Program to Implement Splay Tree
*/
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
struct splay
{
int key;
splay* lchild;
splay* rchild;
};
class SplayTree
{
public:
SplayTree()
{
}
// RR(Y rotates to the right)
splay* RR_Rotate(splay* k2)
{
splay* k1 = k2->lchild;
k2->lchild = k1->rchild;
k1->rchild = k2;
return k1;
}
// LL(Y rotates to the left)
splay* LL_Rotate(splay* k2)
{
splay* k1 = k2->rchild;
k2->rchild = k1->lchild;
k1->lchild = k2;
return k1;
}
// An implementation of top-down splay tree
splay* Splay(int key, splay* root)
{
if (!root)
return NULL;
splay header;
/* header.rchild points to L tree;
header.lchild points to R Tree */
header.lchild = header.rchild = NULL;
splay* LeftTreeMax = &header;
splay* RightTreeMin = &header;
while (1)
{
if (key < root->key)
{
if (!root->lchild)
break;
if (key < root->lchild->key)
{
root = RR_Rotate(root);
// only zig-zig mode need to rotate once,
if (!root->lchild)
break;
}
/* Link to R Tree */
RightTreeMin->lchild = root;
RightTreeMin = RightTreeMin->lchild;
root = root->lchild;
RightTreeMin->lchild = NULL;
}
else if (key > root->key)
{
if (!root->rchild)
break;
if (key > root->rchild->key)
{
root = LL_Rotate(root);
// only zag-zag mode need to rotate once,
if (!root->rchild)
break;
}
/* Link to L Tree */
LeftTreeMax->rchild = root;
LeftTreeMax = LeftTreeMax->rchild;
root = root->rchild;
LeftTreeMax->rchild = NULL;
}
else
break;
}
/* assemble L Tree, Middle Tree and R tree */
LeftTreeMax->rchild = root->lchild;
RightTreeMin->lchild = root->rchild;
root->lchild = header.rchild;
root->rchild = header.lchild;
return root;
}
splay* New_Node(int key)
{
splay* p_node = new splay;
if (!p_node)
{
fprintf(stderr, "Out of memory!\n");
exit(1);
}
p_node->key = key;
p_node->lchild = p_node->rchild = NULL;
return p_node;
}
splay* Insert(int key, splay* root)
{
static splay* p_node = NULL;
if (!p_node)
p_node = New_Node(key);
else
p_node->key = key;
if (!root)
{
root = p_node;
p_node = NULL;
return root;
}
// 先进行旋转
root = Splay(key, root);
/* This is BST that, all keys <= root->key is in root->lchild, all keys >
root->key is in root->rchild. */
// 再insert
if (key < root->key)
{
p_node->lchild = root->lchild;
p_node->rchild = root;
root->lchild = NULL;
root = p_node;
}
else if (key > root->key)
{
p_node->rchild = root->rchild;
p_node->lchild = root;
root->rchild = NULL;
root = p_node;
}
else
return root;
p_node = NULL;
return root;
}
splay* Delete(int key, splay* root)
{
splay* temp;
if (!root)
return NULL;
root = Splay(key, root);
if (key != root->key)
return root;
else
{
if (!root->lchild)
{
temp = root;
root = root->rchild;
}
else
{
temp = root;
/*Note: Since key == root->key,
so after Splay(key, root->lchild),
the tree we get will have no right child tree.*/
root = Splay(key, root->lchild);
root->rchild = temp->rchild;
}
free(temp);
return root;
}
}
splay* Search(int key, splay* root)
{
return Splay(key, root);
}
void InOrder(splay* root)
{
if (root)
{
InOrder(root->lchild);
cout<< "key: " <<root->key;
if(root->lchild)
cout<< " | left child: "<< root->lchild->key;
if(root->rchild)
cout << " | right child: " << root->rchild->key;
cout<< "\n";
InOrder(root->rchild);
}
}
};
int main()
{
SplayTree st;
int vector[10] = {9,8,7,6,5,4,3,2,1,0};
splay *root;
root = NULL;
const int length = 10;
int i;
for(i = 0; i < length; i++) {
root = st.Insert(vector[i], root);
cout<<"\nInOrder: \n";
st.InOrder(root);
}
int input, choice;
if(1)
{
cout<<"\nSplay Tree Operations\n";
cout<<"1. Insert "<<endl;
cout<<"2. Delete"<<endl;
cout<<"3. Search"<<endl;
cout<<"4. Exit"<<endl;
cout<<"Enter your choice: ";
cin>>choice;
switch(choice)
{
case 1:
cout<<"Enter value to be inserted: ";
cin>>input;
root = st.Insert(input, root);
cout<<"\nAfter Insert: "<<input<<endl;
st.InOrder(root);
break;
case 2:
cout<<"Enter value to be deleted: ";
cin>>input;
root = st.Delete(input, root);
cout<<"\nAfter Delete: "<<input<<endl;
st.InOrder(root);
break;
case 3:
cout<<"Enter value to be searched: ";
cin>>input;
root = st.Search(input, root);
cout<<"\nAfter Search "<<input<<endl;
st.InOrder(root);
break;
case 4:
exit(1);
default:
cout<<"\nInvalid type! \n";
}
}
cout<<"\n";
return 0;
}
自顶向上
.hpp
//
// SBTree.hpp
// SBTree
//
// Created by staff on 16/10/8.
// Copyright © 2016年 George1994. All rights reserved.
//
#ifndef SBTree_hpp
#define SBTree_hpp
namespace SBTree {
template <class T>
class TreeNode {
public:
TreeNode(T value) : value(value), left(nullptr), right(nullptr), parent(nullptr) {}
T value;
TreeNode* left;
TreeNode* right;
TreeNode* parent;
};
template <class T>
class Tree {
public:
Tree();
Tree(TreeNode<T>* root) : root(root) {};
~Tree();
static Tree<T>* getInstance();
static void destroyInstance();
void Insert(T value);
void Delete(T value);
TreeNode<T>* Find(T value);
void Traversal();
T mininum();
T maxmum();
private:
TreeNode<T>* root; // 跟节点
static Tree * tree;
void insertNode(TreeNode<T>* &root, TreeNode<T>* node);
void deleteNode(TreeNode<T>* &root, T value);
TreeNode<T>* searchNode(TreeNode<T>* &root, T value);
void splay(TreeNode<T>* &root, T value);
void traversalNode(TreeNode<T>* root);
void destroy(TreeNode<T>* &root);
// zig:右旋 zag:左旋
void singleLeftRotate(TreeNode<T>* &root);
void singleRightRotate(TreeNode<T>* &root);
void zigzagRotate(TreeNode<T>* &root);
void zagzigRotate(TreeNode<T>* &root);
void zigzigRotate(TreeNode<T>* &root);
void zagzagRotate(TreeNode<T>* &root);
T mininum(TreeNode<T>* &root);
T maximum(TreeNode<T> &root);
};
void insertTest();
void deleteTest();
void searchTest();
void teversalTest();
void maxTest();
void minTest();
}
#endif /* SBTree_hpp */
.cpp
//
// SBTree.cpp
// SBTree
//
// Created by staff on 16/10/8.
// Copyright © 2016年 George1994. All rights reserved.
//
#include "SBTree.hpp"
#include <iostream>
#include <string>
#include <cassert>
#include <vector>
namespace SBTree {
typedef Tree<int> ITree;
int getInputInt(const char* content) {
std::cout << content << ":";
int input;
std::cin >> input;
return input;
}
void LogErr(const char* content) {
assert(content);
std::cout << "Error:" << content << std::endl;
}
template <class T>
Tree<T>::Tree() {
root = nullptr;
}
template <class T>
Tree<T>::~Tree() {
destroy();
}
/**
* 插入结点的同时旋转树把结点旋转为跟节点
*
* @param value <#value description#>
*/
template <class T>
void Tree<T>::Insert(T value) {
TreeNode<T>* node = new TreeNode<T>(value);
if (root == nullptr) {
root = node;
return ;
}
insertNode(root, node);
splay(root, value);
}
template <class T>
void Tree<T>::Delete(T value) {
deleteNode(root, value);
}
template <class T>
TreeNode<T>* Tree<T>::Find(T value) {
splay(root, value);
return root;
}
template <class T>
T Tree<T>::mininum() {
if (!root) {
return -1;
}
TreeNode<T> *node = root;
while (node->left) {
node = node->left;
}
splay(root, node->value);
return node->value;
}
template <class T>
T Tree<T>::maxmum() {
if (!root) {
return -1;
}
TreeNode<T> *node = root;
while (node->right) {
node = node->right;
}
splay(root, node->value);
return node->value;
}
/**
* 目标结点是跟结点的右子结点,对子结点进行左旋
*/
template <class T>
void Tree<T>::singleLeftRotate(TreeNode<T>* &node) {
TreeNode<T> *rchild = node->right;
TreeNode<T> *parent = node->parent;
if (rchild->left) {
rchild->left->parent = node;
}
node->right = rchild->left;
rchild->left = node;
rchild->parent = parent;
if (parent) {
if (parent->left == node) {
parent->left = rchild;
}
else {
parent->right = rchild;
}
}
node->parent = rchild;
node = rchild;
}
/**
* 目标结点是跟结点的左子结点,对子结点进行右旋
*/
template <class T>
void Tree<T>::singleRightRotate(TreeNode<T>* &node) {
TreeNode<T> *lchild = node->left;
TreeNode<T> *parent = node->parent;
if (lchild->right) {
lchild->right->parent = node;
}
node->left = lchild->right;
lchild->parent = parent;
lchild->right = node;
if (parent) {
if (parent->left == node) {
parent->left = lchild;
}
else {
parent->right = lchild;
}
}
node->parent = lchild;
node = lchild;
}
/**
* 目标结点在右边,父结点在左边,即之字型
*/
template <class T>
void Tree<T>::zagzigRotate(TreeNode<T>* &node) {
singleLeftRotate(node->left);
singleRightRotate(node);
if (node->parent == nullptr) {
root = node;
}
std::cout << "zagzigRotate" << std::endl;
}
/**
* 目标结点在左边,父结点在右边,即之字型
*/
template <class T>
void Tree<T>::zigzagRotate(TreeNode<T>* &node) {
singleRightRotate(node->right);
singleLeftRotate(node);
if (node->parent == nullptr) {
root = node;
}
std::cout << "zigzagRotate" << std::endl;
}
/**
* 目标结点和父结点和祖父结点均在右边,为zig-zig,即一字型,先左旋转父结点,再左旋转目标结点
*/
template <class T>
void Tree<T>::zigzigRotate(TreeNode<T>* &node) {
TreeNode<T> *father = node->right;
singleLeftRotate(father);
singleLeftRotate(node);
if (node->parent == nullptr) {
root = node;
}
std::cout << "zigzigRotate" << std::endl;
}
/**
* 目标结点和父结点和祖父结点均在左边,为zag-zag,即一字型,先右旋转父结点,再右旋转目标结点
*/
template <class T>
void Tree<T>::zagzagRotate(TreeNode<T>* &node) {
TreeNode<T> *father = node->left;
singleRightRotate(father);
singleRightRotate(node);
if (node->parent == nullptr) {
root = node;
}
std::cout << "zagzagRotate" << std::endl;
}
/**
* 1.当目标结点是根节点的左子结点或者右子结点,进行单旋转(左旋转和右旋转),将目标结点调整到根结点
* 2.当目标结点和父结点和祖父结点构成“之”字形时,进行双旋转(左右旋转和右左旋转),将目标结点调整到祖父结点
* 3.当目标结点和父结点和祖父结点构成“一”字形时,进行zig-zig旋转(左左旋转和右右旋转),将目标结点调整到祖父结点
*
* @param root <#root description#>
* @param value <#value description#>
*/
template <class T>
void Tree<T>::splay(TreeNode<T>* &node, T value) {
TreeNode<T>* nodeparent = node->parent;
TreeNode<T>* fNode = searchNode(root, value);
if (!fNode || (node->left == nullptr && node->right == nullptr)) {
return;
}
while (fNode->parent != nodeparent) {
if (fNode->parent == node) {
// 单旋转情况
if (node->left == fNode) {
singleRightRotate(node);
}
else {
singleLeftRotate(node);
}
node->parent = fNode;
fNode->parent = nodeparent;
}
else {
TreeNode<T> *parent = fNode->parent;
TreeNode<T> *grandparent = parent->parent;
bool fNodeLeft = parent->left == fNode; // 目标结点的左右情况
bool parentLeft = grandparent->left == parent; // 父结点的左右情况
if (parentLeft) {
if (fNodeLeft) {
zagzagRotate(grandparent); // 左左
}
else {
zagzigRotate(grandparent); // 左右
}
}
else {
if (fNodeLeft) {
zigzagRotate(grandparent); // 右左
}
else {
zigzigRotate(grandparent); // 右右
}
}
}
}
}
template <class T>
void Tree<T>::insertNode(TreeNode<T>* &root, TreeNode<T>* node) {
if (node->value > root->value) {
if (root->right == nullptr) {
root->right = node;
node->parent = root;
}
else {
insertNode(root->right, node);
}
}
else if (node->value <= root->value) {
if (root->left == nullptr) {
root->left = node;
node->parent = root;
}
else {
insertNode(root->left, node);
}
}
}
template <class T>
void Tree<T>::deleteNode(TreeNode<T>* &node, T value) {
splay(node, value);
// TreeNode<T> *ntree;
// 用左子树的最大点替换
if (node->left == nullptr) {
node = node->right;
}
else if (node->right == nullptr) {
node = node->left;
}
else {
// 找到左子树的最大结点,并且将其翻转倒左子树的跟结点位置,也就是跟的左结点
// 然后将跟的右结点连到跟的左结点上
TreeNode<T> *rnode = node->right;
TreeNode<T> *tmpnode = node->left;
while (tmpnode->right != nullptr) {
tmpnode = tmpnode->right;
}
splay(node->left, tmpnode->value);
node = node->left;
node->parent = nullptr;
if (rnode) {
rnode->parent = node;
}
node->right = rnode;
}
node->parent = nullptr;
root = node;
}
template <class T>
TreeNode<T>* Tree<T>::searchNode(TreeNode<T>* &root, T value) {
if (root == nullptr) {
return nullptr;
}
if (value > root->value) {
return searchNode(root->right, value);
}
else if (value < root->value) {
return searchNode(root->left, value);
}
else {
return root;
}
}
template <class T>
void Tree<T>::traversalNode(TreeNode<T>* root) {
if (root == nullptr) return;
if (root->left) traversalNode(root->left);
std::cout << root->value;
if (root->parent) {
if (root->left) {
std::cout << " | left child is:" << root->left->value;
}
if (root->right) {
std::cout << " | right child is:" << root->right->value;
}
std::cout << " | parent is:" << root->parent->value << std::endl;
}
else {
std::cout << " | is root" << std::endl;
}
if (root->right) traversalNode(root->right);
}
template <class T>
void Tree<T>::destroy(TreeNode<T>* &root) {
if (root == nullptr) {
return;
}
if (root->left) destroy(root->left);
if (root->right) destroy(root->right);
delete root;
}
template <class T>
T Tree<T>::mininum(TreeNode<T>* &root) {
}
template <class T>
T Tree<T>::maximum(TreeNode<T> &root) {
}
template <class T>
void Tree<T>::Traversal() {
assert(root != nullptr);
traversalNode(root);
std::cout << std::endl;
}
template <class T>
Tree<T>* Tree<T>::tree = nullptr;
template <class T>
Tree<T>* Tree<T>::getInstance() {
if (tree == nullptr) {
tree = new Tree<T>();
}
return tree;
}
template <class T>
void Tree<T>::destroyInstance() {
assert(tree == nullptr);
delete tree;
}
void insertTest() {
for (int i = 0; i <= 9; i = i + 1) {
ITree::getInstance()->Insert(i);
}
}
void searchTest() {
int input = getInputInt("please input the search value");
TreeNode<int> *node = ITree::getInstance()->Find(input);
std::cout << "Node value:" << node->value << std::endl;
}
void deleteTest() {
int input = getInputInt("please input the delete value");
ITree::getInstance()->Delete(input);
}
void teversalTest() {
ITree::getInstance()->Traversal();
}
void maxTest() {
int result = ITree::getInstance()->maxmum();
std::cout << "max:" << result << std::endl;
}
void minTest() {
int result = ITree::getInstance()->mininum();
std::cout << "min:" << result << std::endl;
}
}
上面这个是我实现的,但是还是有各种各样的bug,还待完善吧。。。
Splay-Tree理解的更多相关文章
- [学习笔记] Splay Tree 从入门到放弃
前几天由于出行计划没有更博QwQ (其实是因为调试死活调不出来了TAT我好菜啊) 伸展树 伸展树(英语:Splay Tree)是一种二叉查找树,它能在O(log n)内完成插入.查找和删除操作.它是由 ...
- 云心出岫——Splay Tree
(多图预警!!!建议在WI-FI下观看) 之前我们谈论过AVL树,这是一种典型适度平衡的二叉搜索树,成立条件是保持平衡因子在[-1,1]的范围内,这个条件已经是针对理想平衡做出的一个妥协了,但依然显得 ...
- BZOJ1500 [NOI2005]维修数列(Splay tree)
[Submit][Status][Discuss] Description 请写一个程序,要求维护一个数列,支持以下 6 种操作: 请注意,格式栏 中的下划线‘ _ ’表示实际输入文件中的空格 Inp ...
- 纸上谈兵:伸展树(splay tree)
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每次 ...
- bzoj 3223/tyvj 1729 文艺平衡树 splay tree
原题链接:http://www.tyvj.cn/p/1729 这道题以前用c语言写的splay tree水过了.. 现在接触了c++重写一遍... 只涉及区间翻转,由于没有删除操作故不带垃圾回收,具体 ...
- 伸展树 Splay Tree
Splay Tree 是二叉查找树的一种,它与平衡二叉树.红黑树不同的是,Splay Tree从不强制地保持自身的平衡,每当查找到某个节点n的时候,在返回节点n的同时,Splay Tree会将节点n旋 ...
- [转] Splay Tree(伸展树)
好久没写过了,比赛的时候就调了一个小时,差点悲剧,重新复习一下,觉得这个写的很不错.转自:here Splay Tree(伸展树) 二叉查找树(Binary Search Tree)能够支持多种动态集 ...
- 树-伸展树(Splay Tree)
伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...
- HDU-3436 Queue-jumpers 树状数组 | Splay tree删除,移动
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3436 树状数组做法<猛戳> Splay tree的经典题目,有删除和移动操作.首先要离散化 ...
- HDU1890 Robotic Sort Splay tree反转,删除
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1890 题目中涉及数的反转和删除操作,需要用Splay tree来实现.首先对数列排序,得到每个数在数列 ...
随机推荐
- 解决java计算中double类型结果不一致问题,使用BigDecimal解决
一.需求:从数据表中读出一个double的数据,比如是3.5,没问题,但是如果再用3.5进行计算,比如乘以100,结果就是350了,而是35000000004 因为是浮点运算,所有语言中的浮点数都会有 ...
- 一个简单的java jdbc案例
有些时候,配置一个spring+mybatis框架,然后写xml,dao ,service显得特别繁琐. 如果我们只是想查一下数据库,不考虑连接复用也不考虑动态sql,可以用原生的jdbc来实现,方便 ...
- C#里partial关键字的作用
1. 什么是局部类型?C# 2.0 引入了局部类型的概念.局部类型允许我们将一个类.结构或接口分成几个部分,分别实现在几个不同的.cs文件中.局部类型适用于以下情况: (1) 类型特别大,不宜放在一个 ...
- centos7 yum搭建lamp环境
=============================================== 2018/1/14_第1次修改 ccb_warlock == ...
- Go 1 Release Notes
Go 1 Release Notes Introduction to Go 1 Changes to the language Append Close Composite literals Goro ...
- 左列動態添加菜單Repeater
前台代碼: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="left.aspx. ...
- head命令 tail命令
head命令 head命令用于显示文件的开头的内容.在默认情况下,head命令显示文件的头10行内容. -n<数字>:指定显示头部内容的行数: -c<字符数>:指定显示头部内容 ...
- 为django平台生成模拟用户,建立用户组,并将用户加入组
书接上篇BLOG. 当我们可以用manage.py自定义命令来生成模拟数据时, 我们面对的就是如何操作ORM的问题了. 这两天,我为我们的内部系统的所有数据表,都生成了模拟数据. 有几个心得,记录于此 ...
- CCF CSP 201703-5 引水入城(50分)
CCF计算机职业资格认证考试题解系列文章为meelo原创,请务必以链接形式注明本文地址 CCF CSP 201703-5 引水入城 问题描述 MF城建立在一片高原上.由于城市唯一的水源是位于河谷地带的 ...
- Java第三阶段学习(三、字符流、转换流)
一.字节流读取中文时出现的问题: 文件中有中文时,用字节流读取会出现乱码的问题,因为一个中文为两个字节. 二.字符编码表 编码表:其实就是生活中字符和计算机二进制的对应关系表. 1.ascii: 一个 ...