zoukankan      html  css  js  c++  java
  • DS博客作业02--栈和队列

    0.PTA得分截图


    1.本周学习总结

    1.1 总结栈和队列内容

    • 栈的基本认识
      栈(stack):栈是限定仅在表的一端进行插入或删除操作的线性表。
      我们把允许插入和删除操作的一端称为栈顶(top),另一端称为栈底(bottom)。不含任何数据元素的栈称为空栈。栈又称为“后进先出(Last In First Out,简称LIFO)的线性表”,简称为LIFO结构。
      栈的插入操作,称为进栈/入栈/压栈。
      栈的删除操作,称为出栈/弹栈。
      不过要注意的是,最先进栈的元素不代表最后出栈。栈对线性表的插入删除位置做了限制,但并没有对出栈和入栈的时间做限制。也就是说,在不是所有元素都入栈的情况下,事先入栈的元素也可以在任意时间出栈,只要保证每次出栈的元素都是栈顶元素就可以。
    • 栈的顺序存储结构
      顺序栈是栈的顺序实现。顺序栈是指利用顺序存储结构实现的栈。采用地址连续的存储空间(数组)依次存储栈中数据元素,由于人栈和出栈运算都是在栈顶进行,而栈底位置是固定不变的,可以将栈底位置设置在数组空间的起始处;栈顶位置是随入栈和出栈操作而变化的,故需用一个整型变量top来记录当前栈顶元素在数组中的位置
      顺序栈的结构定义:
    typedef int SElemType;
    typedef int Status; 
    struct SqStack
    {
    	SElemType *base;
    	SElemType *top;
    	int stacksize;
    };
    
    • 顺序栈的操作

    1.栈的初始化

    Status InitStack(SqStack &S)
    {
    	S.base = (SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType));
    	if(!S.base) return OVERFLOW;
    	S.top = S.base;
    	S.stacksize = STACK_INIT_SIZE;
    	return OK;
    }
    

    2.销毁栈

    Status DestoryStack(SqStack &S)
    {
    	free(S.base);
    	S.base = NULL;
    	S.top = NULL;
    	S.stacksize = 0;
    	return OK;
    }
    

    3.清空栈

    Status ClearStack(SqStack &S)
    {
    	S.top = S.base;
    	return OK;
    }
    

    4.判断栈空

    Status StackEmpty(SqStack S)
    {
    	if(S.top == S.base)
    		return TRUE;
    	else
    		return FALSE;
    }
    

    5.取栈长

    int StackLength(SqStack S)
    {
    	return S.top - S.base;
    }
    

    6.取栈顶

    Status GetTop(SqStack S,SElemType &e)
    {
    	if(S.top == S.base) return ERROR;
    	e = *(S.top -1);
    	return OK;
    }
    

    7.入栈

    Status Push(SqStack &S, SElemType e)
    {
    	if(S.top - S.base >= S.stacksize)
    	{
    		S.base = (SElemType *)realloc(S.base,(S.stacksize + STACKINCREMENT)*sizeof(SElemType));
    		if(!S.base) return OVERFLOW;
    		S.top = S.base + S.stacksize;
    		S.stacksize += STACKINCREMENT; 
    	}
    	*S.top++ = e;
    	return OK;
    }
    

    8.出栈

    Status Pop(SqStack &S, SElemType &e)
    {
    	if(S.top == S.base) return ERROR;
    	e = * --S.top;
    	return OK;
    }
    
    • 栈的链式存储结构
      对于顺序栈来说,主要的缺点就是栈的大小已经固定,若有超过栈长的元素个数,则此时栈会发生“溢出”。这时我们可以采用链式栈的存储结构,这样就不用再考虑栈的空间是否足够大的问题。
      栈的链式存储结构,简称为链栈。
      思考:对于栈的链式存储结构来说,栈顶指针是在链表头结点位置更好,还是在链表尾节点位置更好?
      答:头结点位置更好
      链表有头指针,而栈的主要操作也是在栈顶进行,那么我们就可以将二者合一,将单链表的头指针作为栈顶指针,即栈的链式存储结构的栈顶指针为单链表的头指针。
      链栈的结构定义:
    typedef char ElemType;
    typedef struct linknode
    {
        ElemType data;
        struct linknode *next;
    }LinkStNode;LinkStNode *s;
    
    • 链栈的操作

    1.初始化栈

    void InitStack(LinkStNode*&s)//初始化栈
    {
        cout<<"初始化栈"<<endl;
        s=(LinkStNode*)malloc(sizeof(LinkStNode));
        s->next=NULL;
    }
    

    2.进栈

    void Push(LinkStNode*&s)//进栈
    {
        ElemType e;
        int i=0;
        LinkStNode *p;
        cout<<"依次进栈的元素为:";
        while (i<n)
        {
            cin>>e;
            p=(LinkStNode*)malloc(sizeof(LinkStNode));
            p->data=e;
            p->next=s->next;
            s->next=p;
            i++;
        }
    }
    

    3.判断栈空

    void StackEmpty(LinkStNode *s)//判断栈是否为空
    {
        if(s->next==NULL)
        {
            cout<<"栈为空"<<endl;
        }
        else
            cout<<"栈非空"<<endl;
    }
    

    4.取栈顶

    void GetTop(LinkStNode*s)//得到栈顶元素
    {
        if(s->next!=NULL)
        {
            cout<<"栈顶元素为:";
            cout<<s->next->data;
            cout<<endl;
        }
    }
    

    5.出栈

    void Pop(LinkStNode *&s)//出栈操作
    {
        cout<<"出栈序列为:";
        LinkStNode *p;
        while(s->next!=NULL)
        {
            cout<<s->next->data<<" ";
            p=s->next;
            s->next=p->next;
            free(p);
        }
        cout<<endl;
    }
    

    6.销毁栈

    void DestroyStack(LinkStNode*&s)//释放栈
    {
        cout<<"释放栈";
        LinkStNode *q=s,*p=s->next;
        while(p!=NULL)
        {
            free(q);
            q=p;
            p=q->next;
        }
        free(q);
    }
    
    • 栈的应用:后缀表达式

    对于数学运算来说,确定运算符的优先级是十分重要的,直接决定了该算式是否计算正确。在实际生活中,我们书写的算式都是中缀表达式,即运算符(此处特指算数运算符)在操作数中间。例如:

    9+(3-1)*3+10/2

    我们把这种平时使用的四则运算表达式的写法称为中缀表达式。但是对于计算机而言,中缀表达式并不方便。计算机计算都是从左到右顺序计算,在该算式中,*在+之后,但是却要先于+进行运算,而加入括号后,运算则会变得更加复杂。
    对于四则运算,20世纪50年代,波兰逻辑学家Jan Lukasiewicz发明了一种不需要括号的表达式方法,称为后缀表示法,也称为逆波兰(Reverse Polish Notation,简称RPN)表示法。

    那么,如何把中缀表达式转化为后缀表达式呢?方法:
    从左至右遍历中缀表达式的每个数字和符号,按照以下规则,直到最终输出后缀表达式:

    使用map容器将-+和1配对,*/和2配对,(和3配对
    for 遍历字符串
    	if 遇到) then 一直出栈至遇到(为止
    	else if 遇到数字或者小数点 then  直接出栈
    	else if 遇到(+ || -)&&(在第一位 || 前面不是数字不是) ) (为正负号) then 
    		直接出栈
    	else if 遇到运算符 then
     		while 栈不空 
        			if 运算符优先级大于栈顶优先级 或 栈顶为( 
    			直接入栈
         			否则 出栈
    			end if
    		end while
     		if 栈空 then 运算符入栈
    	end if
    遍历完字符串后 if 栈不空 then
    	一直出栈至栈空
    end if
    

    我们以下面的算式为例进行讲解

    9+(3-1)*3+10/2------>9 3 1 - 3 * + 10 2 / +

    1.初始化一个空栈,用于对符号进出栈使用。
    2.第一个数字是9,输出9。后面的符号+入栈。
    3.第三个字符是(,依然是符号,因其是左括号还未配对,故进栈。
    4.第四个字符是数字3,输出,此时表达式为9 3,接着符号-进栈。
    5.接下来是数字1,输出,此时表达式为9 3 1,后面是符号),此时我们需要把(之前的所有元素都出栈,直至输出(为止。此时总的表达式是9 3 1 -。
    6.紧接着是符号,因为此时的栈顶符号是+,优先级低于,因此不输出,进栈。紧接着是数字3,输出,总表达式为9 3 1 – 3.
    7.之后是符号+,此时栈顶元素是
    ,比+优先级高,因此栈中元素出栈并输出(因为没有比+更低优先级的符号,所以全部出栈),总输出表达式为9 3 1 – 3 * +。然后将这个符号+进栈。
    8.紧接着输出数字10,总表达式为9 3 1 – 3 * + 10。之后是符号/,所以/进栈。
    9.最后一个数字为2,此时总表达式为9 3 1 – 3 * + 10 2。
    10.因已到最后,所以将栈中符号全部出栈。最终获得的后缀表达式为9 3 1 – 3 * + 10 2 / +。

    具体的代码实现如下:

    #include<iostream>
    #include<map>
    #include<stack>
    #include<string>
    
    using namespace std;
    
    void InfixToSuffix(string infix, string& suffix);
    
    int main() 
    {
        string infix;
        string suffix;
        cin >> infix;
        InfixToSuffix(infix, suffix);
        cout << suffix;
        return 0;
    }
    
    void InfixToSuffix(string infix, string& suffix)
    {
        int i;
        int len;
        map<char,int> op;
        stack<char> oper;
    
        len = infix.length();
        op['-'] = 1;op['+'] = 1;op['*'] = 2;op['/'] = 2;op['('] = 3;
        for (i = 0;i <= len;i++)
        {
            if (infix[i] == ')')
            {
                while (oper.top() != '(')
                {
                    suffix = suffix + oper.top() + ' ';
                    oper.pop();
                }
                oper.pop();
            }
            else if (isdigit(infix[i])||infix[i]=='.')
            {
                while (isdigit(infix[i]) || infix[i] == '.')
                {
                    suffix += infix[i];
                    i++;
                }
                suffix += ' ';
                i--;
            }
            else if ((infix[i] == '-' || infix[i] == '+') && (i == 0 || (infix[i - 1] != ')' && !isdigit(infix[i - 1]))))
            {
                if (infix[i] == '-')
                {
                    suffix += infix[i];
                }
            }
            else
            {
                while (!oper.empty())
                {
                    if (op[infix[i]] > op[oper.top()] || oper.top() == '(')
                    {
                        oper.push(infix[i]); 
                        break;
                    }
                    else
                    {
                        suffix = suffix + oper.top() + ' ';
                        oper.pop();
                    }
                }
                if (oper.empty())
                {
                    oper.push(infix[i]);
                }
            }
        }
        while (!oper.empty())
        {
            suffix = suffix + oper.top() + ' ';
            oper.pop();
        }
        suffix.erase(suffix.length() - 3);
    }
    
    

    队列

    • 队列的基本认识

    队列(queue):队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
    队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入操作的一端称为队尾,允许删除操作的一端称为队头。
    队列与现实生活中的排队机制很像,排在队头的出队,而想入队则只能从队尾开始。
    队列的基本操作包括:

    初始化队列:InitQueue(Q) 
       操作前提:Q为未初始化的队列。 
       操作结果:将Q初始化为一个空队列。
    判断队列是否为空:IsEmpty(Q) 
       操作前提:队列Q已经存在。 
       操作结果:若队列为空则返回1,否则返回0。
    判断队列是否已满:IsFull(Q) 
       操作前提:队列Q已经存在。 
       操作结果:若队列为满则返回1,否则返回0。
    入队操作:EnterQueue(Q,data) 
       操作前提:队列Q已经存在。 
       操作结果:在队列Q的队尾插入data。
    出队操作:DeleteQueue(Q,&data) 
       操作前提:队列Q已经存在且非空。 
       操作结果:将队列Q的队头元素出队,并使用data带回出队元素的值。
    取队首元素:GetHead(Q,&data) 
       操作前提:队列Q已经存在且非空。 
       操作结果:若队列为空则返回1,否则返回0。
    清空队列:ClearQueue(&Q) 
       操作前提:队列Q已经存在。 
       操作结果:将Q置为空队列。
    
    • 队列的顺序存储结构之循环队列
      队列的顺序存储有很大的缺陷,会造成大量的已出队的元素的存储空间浪费。而且,若此时入队元素已经大于n,则我们需要更大的存储空间才行,但队头位置有大量空间未利用,空间浪费严重。
      解决以上问题的方法就是如果后面满了,则我们就从头开始,也就是将队列做成头尾相接的循环。我们把这种头尾相接的顺序存储结构的队列称为循环队列。
      循环队列的结构定义:
    typedef int DataType;  //队列中元素类型
    typedef struct Queue
    {
        DataType Queue[MaxSize];
        int fornt;       //队头指针
        int rear;        //队尾指针
    }SeqQueue;
    
    • 循环队列的基本操作

    1.初始化队列:InitQueue(Q)

    //队列初始化,将队列初始化为空队列
    void InitQueue(SeqQueue *SQ)
    {
        SQ->fornt = SQ->rear = 0;  //把对头和队尾指针同时置0
     }
    

    2.判断队列是否为空:IsEmpty(Q)

    //判断队列为空
    int IsEmpty(SeqQueue* SQ)
    {
        if (SQ->fornt == SQ->rear)
        {
            return 1;
        }
        return 0;
    }
    

    3.判断队列是否已满:IsFull(Q)

    //判断队列是否为满
    int IsFull(SeqCirQueue* SCQ)
    {
        //尾指针+1追上队头指针,标志队列已经满了
        if ((SCQ->rear + 1) % MaxSize == SCQ->fornt)
        {
            return 1;
        }
        return 0;
    }
    

    4.入队操作:EnterQueue(Q,data)

    int EnterSeqCirQueue(SeqCirQueue* SCQ, DataType data)
    {
        if (IsFull(SCQ))
        {
            printf("队列已满,不能入队!
    ");
            return 0;
        }
        SCQ->Queue[SCQ->rear] = data;
        SCQ->rear = (SCQ->rear + 1) % MaxSize;   //重新设置队尾指针
    }
    

    5.出队操作:DeleteQueue(Q,&data)

    int DeleteSeqCirQueue(SeqCirQueue* SCQ,DataType* data)
    {
        if (IsEmpty(SCQ))
        {
            printf("队列为空!
    ");
            return 0;
        }
        *data = SCQ->Queue[SCQ->fornt];
        SCQ->fornt = (SCQ->fornt + 1) % MaxSize;  //重新设置队头指针
    }
    

    6.取队首元素:GetHead(Q,&data)

    int GetHead(SeqCirQueue* SCQ,DataType* data)
    {
        if (IsEmpty(SCQ))
        {
            printf("队列为空!
    ");
            return 0;
        }
        *data = SCQ->Queue[SCQ->fornt];
        return *data;
    }
    

    7.清空队列:ClearQueue(&Q)

    void ClearSeqCirQueue(SeqCirQueue* SCQ)
    {
        SCQ->fornt = SCQ->rear = 0;
    }
    
    • 队列的链式存储结构

    队列的链式存储结构简称为链式队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表。链队的操作实际上是单链表的操作,只不过是出队在表头进行,入队在表尾进行。入队、出队时分别修改不同的指针。
    链式队列的结点是动态开辟的,入队时,为新节点开辟空间,出队使释放出队元素结点的空间。所以相对于顺序队列和循环队列,链式队列没有判断队列是否为满操作。但在清空队列时需要将队列所有结点的空间动态释放,从而防止内存泄露。
    链式队列的结构定义:

    typedef int DataType;
    typedef struct Node
    {
        DataType _data;
        struct Node* _next;
    }LinkQueueNode;
    
    typedef struct
    {
        LinkQueueNode* front;
        LinkQueueNode* rear;
    }LinkQueue;
    
    • 链式队列的操作

    1.初始化队列

    //初始化队列
    void InitLinkQueue(LinkQueue* LQ)
    {
        //创建一个头结点
        LinkQueueNode* pHead = (LinkQueueNode*)malloc(sizeof(LinkQueueNode));
        assert(pHead);
        LQ->front = LQ->rear = pHead; //队头和队尾指向头结点
        LQ->front->_next = NULL;
    }
    

    2.判断队列是否为空

    //判断队列是否为空
    int IsEmpty(LinkQueue* LQ)
    {
        if (LQ->front->_next == NULL)
        {
            return 1;
        }
        return 0;
    }
    

    3.入队

    //入队操作
    void EnterLinkQueue(LinkQueue* LQ, DataType data)
    {
        //创建一个新结点
        LinkQueueNode* pNewNode = (LinkQueueNode*)malloc(sizeof(LinkQueueNode));
        assert(pNewNode);
        pNewNode->_data = data;  //将数据元素赋值给结点的数据域
        pNewNode->_next = NULL;  //将结点的指针域置空
        LQ->rear->_next = pNewNode;   //将原来队列的队尾指针指向新结点
        LQ->rear = pNewNode;      //将队尾指针指向新结点
    }
    

    4.出队

    //出队操作
    void DeleteLinkQueue(LinkQueue* LQ,DataType* data)
    {
        if (IsEmpty(LQ))
        {
            printf("队列为空!
    ");
            return;
        }
        //pDel指向队头元素,由于队头指针front指向头结点,所以pDel指向头结点的下一个结点
        LinkQueueNode* pDel = LQ->front->_next;  
        *data = pDel->_data;   //将要出队的元素赋给data
        LQ->front->_next = pDel->_next;  //使指向头结点的指针指向pDel的下一个结点
        //如果队列中只有一个元素,将队列置空
        if (LQ->rear = pDel)   
        {
            LQ->rear = LQ->front;
        }
        free(pDel);   //释放pDel指向的空间
    }
    

    5.取队头元素

    //取队头元素
    int GetHead(LinkQueue* LQ, DataType* data)
    {
        if (IsEmpty(LQ))
        {
            printf("队列为空!
    ");
            return 0;
        }
        LinkQueueNode* pCur;
        pCur = LQ->front->_next;  //pCur指向队列的第一个元素,即头结点的下一个结点
        *data = pCur->_data;      //将队头元素值赋给data
        return *data;             //返回队头元素值
    }
    

    6.清空队列

    //清空队列
    void ClearQueue(LinkQueue* LQ)
    {
        while (LQ->front != NULL)
        {
            LQ->rear = LQ->front->_next;  //队尾指针指向队头指针的下一个结点
            free(LQ->front);              //释放队头指针指向的结点
            LQ->front = LQ->rear;         //队头指针指向队尾指针
        }
    }
    
    • 队列应用:报数游戏

    题干:报数游戏是这样的:有n个人围成一圈,按顺序从1到n编好号。从第一个人开始报数,报到m(m<n)的人退出圈子;下一个人从1开始报数,报到m的人退出圈子。如此下去,直到留下最后一个人。其中n是初始人数;m是游戏规定的退出位次(保证为小于n的正整数)。

    分析:使用队列可以很好的模拟报数游戏,如果报数为m那么退出队列,否则重新进入队列,重复此过程直至队列为空,即所有人都退出圈子

    解题思路(以伪代码的形式呈现):

    输入队列
    while 队列中有元素
    	出队
    	if 报数为m then 输出该元素
    	else 入队
    	end if
    end while
    

    具体实现代码如下:

    #include <iostream>
    #include <string>
    #include <queue>
    
    using namespace std;
    
    void CountGame(queue<int> qu, int m, int n);
    
    int main()
    {
    	queue<int> qu;
    	int n;
    	int m;
    	int i;
    
    	cin >> n >> m;
    	for (i = 1;i <= n;i++)
    	{
    		qu.push(i);
    	}
    	CountGame(qu, m, n);
    	return 0;
    }
    
    void CountGame(queue<int> qu, int m, int n)
    {
    	if (m >= n)
    	{
    		cout << "error!";
    		return;
    	}
    
    	int temp;
    	int num = 0;
    	int flag = 1;
    
    	while (!qu.empty())
    	{
    		temp = qu.front();
    		qu.pop();
    		num++;
    		if (num == m)
    		{
    			if (flag)
    			{
    				cout << temp;
    				flag = 0;
    			}
    			else cout << " " << temp;
    			num = 0;
    		}
    		else qu.push(temp);
    	}
    }
    

    关于顺序存储和链式存储的选择,总体来说

    • 若可以大致确定元素个数的情况下,推荐使用顺序存储
    • 无法事先预知元素个数,则应使用链式存储

    1.2.谈谈你对栈和队列的认识及学习体会

    • 对栈和队列的认识
      栈是限定仅在表尾进行插入和删除操作的线性表,是一种具有后进先出的数据结构,又称为后进先出的线性表,也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。
      队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。和栈一样,队列是一种操作受限制的线性表。队列是一种先进先出的数据结构,又称为先进先出的线性表,也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。
    • 学习体会
      最近学习了栈和队列的知识,从最初的看到就懵逼到现在对它们有一个清晰的认识,并能够进行简单的应用,就说明还是学到了一点东西。栈和队列这两种数据结构可以很轻易地解决某些特定的问题,也难怪老师说选对了数据结构,题目就解决了一半多。好好学习,天天向上!

    2.PTA实验作业

    2.1 表达式转换

    2.1.1 代码截图

    2.1.2 本题PTA提交列表说明

    • PTA提交列表

    • 说明

    错误 解决办法
    多种错误 将运算符栈为空的情况单独考虑,并修正输出格式

    2.2 符号配对

    2.2.1 代码截图

    2.2.2 本题PTA提交列表说明

    • PTA提交列表

    • 说明

    错误 解决办法
    Map容器转化反了 将右符号转化成左符号
    /*可能是乘除号 限制/后是或者后是/才是符号
    开头多余左符号没有判断 最后判断栈是否空,空则配对,不空则右符号不匹配

    3.阅读代码

    3.1 题目及解题代码

    题干

    题解

    3.1.1 该题的设计思路

    设计思路:将数字直接放入栈内,遇到其他字符转化为对应的数字存入栈内,最后将栈内的数字一一取出相加就是得分的总和
    时间复杂度:O(n)。需要遍历一次字符数组,遍历一次栈。
    空间复杂度:O(n)。分配空间给了int型的index和res,以及一个栈。

    3.1.2 该题的伪代码

    for 遍历字符串
    	if 是"C" then 出栈
    	else		
    		if 是"+" then tmp=栈内头两个数相加
    		else if 是"D" then tmp=栈顶*2
    		else tmp=得分
    		入栈tmp
    		end if
    	end if
    for 出栈所有元素
    	累加到res中
    end for
    返回res
    

    3.1.3 运行结果

    3.1.4分析该题目解题优势及难点

    • 解题优势

      • 使用vector容器,就不用再考虑需要多少空间
      • 将对应的操作字符转化为数字,统一了数据类型
      • 使用for+auto遍历栈方便快捷
    • 难点:

      • 如何将操作字符转化为对应的数据

    3.2 题目及解题代码

    题干

    题解

    3.2.1 该题的设计思路

    设计思路:

    • 先排身高更高的,这是要防止后排入人员影响先排入人员位置
    • 每次排入新人员[h,k]时,已处于队列的人身高都>=h,所以新排入位置就是people[k]

    时间复杂度:O(n)。sort排序一次,for循环遍历list,重建vector容器
    空间复杂度:O(n)。新建list存放people中的数据,重建vector存放排序好的数据

    3.2.2 该题的伪代码

    将people按照身高降序排序
    相同身高需要按k升序排序
    新建list容器tmp临时存放people中的数据
    for 遍历tmp
    	寻找并将数据插入对应位置
    end for
    返回 根据tmp新建的vector容器
    

    3.2.3 运行结果

    3.1.4分析该题目解题优势及难点

    • 解题优势:
      • 使用sort函数,并重载比较函数,很方便的按照所需将数据排序好
      • 使用list容器进行插入操作,代码的执行效率更高
    • 难点
      • 如何在存在k限制的情况下将身高排序好

  • 相关阅读:
    winform只允许一个应用程序运行
    IIS配置文件的XML格式不正确 applicationHost.config崩溃 恢复解决办法
    C#ToString() 格式化数值
    SQLServer2008只能编辑前面200行数据
    Validform验证时可以为空,否则按照指定格式验证
    js操作cookie
    div z-index无论设置多高都不起作用
    Tableau 练习题
    Tableau可视化操作
    Tableau 基础
  • 原文地址:https://www.cnblogs.com/zzhmyblog/p/12540576.html
Copyright © 2011-2022 走看看