数据结构学习

目录

线性表

顺序表

链表

栈和队列

栈(Stack)

队列

 双端队列

优先队列 (Priority Queue)

字符串

字符串的概念

字符串的实现

字符串的模式匹配

多维数组和广义表

数组

特殊矩阵的压缩存储

稀疏矩阵

广义表




线性表

定义  n(≥0)个数据元素的有限序列

特点   线性排列 除第一个元素外,其他每一个元素有一个且仅有一个直接前趋。 除最后一个元素外,其他每一个元素有一个且仅有一个直接后继。

要点

 表中元素具有逻辑上的顺序性,在序列中各元素排列有其先后次序,有唯一的首元素和尾元素。

表中元素个数有限。

表中元素都是数据元素。即每一表元素都是原子数据,不允许“表中套表”。

表中元素的数据类型都相同。这意味着每一表元素占有相同数量的存储空间。

顺序表

Sequential List

定义  将线性表中的元素相继存放在一个连续的存储空间中,即构成顺序表。

存储  它是线性表的顺序存储表示,可利用一维数组描述存储结构。

特点  元素的逻辑顺序与物理顺序一致。

访问方式  可顺序存取,可按下标直接存取。

LOC(i) = LOC(i-1) + l = a + i * l,       

LOC 是元素存储位置,l 是元素大小

LOC(i) =a,         i = 0    

顺序表的动态结构定义

顺序表动态定义,它可以扩充,新的大小计入数据成员maxSize中。

#define initSize 100
typedef int DataType;
typedef struct {
    DataType *data;   //存储数组
    int n;           //当前表元素个数
    int maxSize;        //表的最大长度
}SeqList;

构造一个空的顺序表

void InitList(SeqList& L){  //构造一个空的顺序表
    L.data=(DataType*)malloc(initSize*sizeof(DataType));
    if(L.data==NULL){
        printf("存储分配失败!\n");
        exit(1);
    }
    L.n=0;
    L.maxSize=initSize;
}

安值查找

int Find(SeqList& L,DataType x){  //按值查找
    for(int i=0;i<L.n;i++){
        // printf("%d %d\n", L.data[i],x);
        if(L.data[i]==x) return i+1;
    }
    return 0;
}  //ACN=(1+n)/2

按位插入

bool insert(SeqList& L,DataType x,int i){ //在第i个位置插入新元素x
    if(L.n==L.maxSize) return false;  //超内存
    if(i<1||i>L.n+1) return false;    //runtime error
    for(int j=L.n-1;j>=i-1;j--) L.data[j+1]=L.data[j];
    L.data[i-1]=x;
    L.n++;
    return true;
} //   n/2

维护有序表

自以为是写了个线段树,结果写完一看复杂度(O(n+log(n))还不如一个一个找(O(n))

bool insert1(SeqList& L,DataType x,int l,int r){   //维护有序顺序表
    if(L.n==L.maxSize) return false;
    if(l==r) {
        insert(L,x,l+1);
        return true;
    }
    int mid=l+r>>1;
    if(L.data[mid]>x) insert1(L,x,l,mid);
    else if(L.data[mid]<x) insert1(L,x,mid+1,r);
    else insert(L,x,mid+1);
    return true;
}

按位删除

int remove1(SeqList& L,int i){   //按位删除
    if(L.n>0&&i>0&&i<=L.n){
        int x=L.data[i-1];
        for(int j=i;j<L.n;j++) L.data[j-1]=L.data[j];
        L.n--;
        return x;
    }
    else return 0;
}   //AMN=(n-1)/2

一些作业(依赖前面几个函数

void remove2(SeqList& L, int x){  //按值删除(第一个
    for(int i=0;i<L.n;i++){
        if(L.data[i]==x) remove1(L,i+1);
    }
}
bool removerange(SeqList& L,int s,int t){  // 按值区间删除
    if(s>t){
        printf("s>t\n");
        return false;
    }
    if(L.n==0){
        printf("表为空\n");
        return false;
    }
    for(int i=0;i<L.n;i++){
        if(L.data[i]>s&&L.data[i]<t){
            remove1(L,i+1);
        }
    }
    return true;
}
void set4(SeqList& L){    //去重
    map<int,int> mp;
    for(int i=0;i<L.n;i++){
        if(mp.find(L.data[i])==mp.end()){
            mp[L.data[i]]=1;
        }
        else{
            remove1(L,i+1);
        }
    }
}
int removemin(SeqList& L){   //删除最小值
    DataType m=0x3f3f3f3f;
    int p;
    if(L.n){
        for(int i=0;i<L.n;i++){
            if(L.data[i]<m){
                p=i;
                m=L.data[i];
            }
        }
        if(L.n==1){
            L.data[0]=0;
        }
        else{
            L.data[p]=L.data[L.n-1];
            L.data[L.n-1]=0;
        }
        L.n--;
        return m;
    }
    printf("表为空\n");
    exit(0);
}

自以为写的还行的几个函数

SeqList merge(SeqList& L1, SeqList& L2){   //合并两有序表
    if(L1.n+L2.n>initSize) {printf("不行\n");exit(1);}
    SeqList L3;
    InitList(L3);
    int i=0,j=0,k=0;
    while(i<L1.n&&j<L2.n){
        if(L1.data[i]<L2.data[j]) L3.data[k++]=L1.data[i++];
        else L3.data[k++]=L2.data[j++];
    }
    while(i<L1.n) L3.data[k++]=L1.data[i++];
    while(j<L2.n) L3.data[k++]=L2.data[j++];
    L3.n=k;
    return L3;
}
int upper(SeqList& L,DataType x){   //手写upper_bound,多少有点大病
    int l=0,r=L.n-1;
    while(l<r){
        int mid=l+(r-l>>1);
        // printf("%d %d\n", l,r);
        if(x<L.data[mid]) r=mid;
        else l=mid+1;
    }
    return r<L.n&&x<L.data[r] ? r : L.n;
}
int lower(SeqList& L,DataType x){
    int l=0,r=L.n-1;
    while(l<=r){
        int mid=l+((r-l)>>1);
        // printf("%d %d\n", l, r);
        if(L.data[mid]<x) l=mid+1;
        else r=mid-1;
    }
    return l<L.n&&L.data[l]>=x ? l:L.n;
}
void removerange1(SeqList& L,int l,int r){
    if(r>=L.n) r=L.n-1;
    int d=r-l+1;
    for(int i=l;i+d<L.n;i++){
        L.data[i]=L.data[i+d];
    }
    L.n-=d;
}
bool removesortedrange(SeqList& L,int s,int t){   //有序表按区间值删除
    if(s>t){
        printf("s>t!\n");
        return false;
    }
    if(!L.n){
        printf("表为空\n");
        return false;
    }
    int l=lower(L,s);
    int r=upper(L,t);
    // printf("%d %d\n", l,r);
    removerange1(L,l,r-1);
    return true;
}

一个标准的

#include<bits/stdc++.h>
#define initSize 100
typedef int DataType;
typedef struct {
    DataType *data;   //存储数组
    int n;           //当前表元素个数
    int maxSize;        //表的最大长度
}SeqList;
void InitList(SeqList& L){  //构造一个空的顺序表
    L.data=(DataType*)malloc(initSize*sizeof(DataType));
    if(L.data==NULL){
        printf("存储分配失败!\n");
        exit(1);
    }
    L.n=0;
    L.maxSize=initSize;
}
int Find(SeqList& L,DataType x){  //按值查找
    for(int i=0;i<L.n;i++){
        // printf("%d %d\n", L.data[i],x);
        if(L.data[i]==x) return i+1;
    }
    return 0;
}  //ACN=(1+n)/2
bool Insert(SeqList& L,DataType x,int i){ //在第i个位置插入新元素x
    if(L.n==L.maxSize) return false;  //超内存
    if(i<1||i>L.n+1) return false;    //runtime error
    for(int j=L.n-1;j>=i-1;j--) L.data[j+1]=L.data[j];
    L.data[i-1]=x;
    L.n++;
    return true;
}
bool Remove(SeqList& L,int i,DataType& x){  //在表中删除第 i 个元素,通过 x 返回其值
    if(L.n>0&&i>0&&i<=L.n){
        x=L.data[i-1];
        for(int j=i;j<L.n;j++) L.data[j-1]=L.data[j];
        L.n--;
        return true;
    }
    else return false;
}   //AMN=(n-1)/2

int main(){
    SeqList lst;
    InitList(lst);
    int n,m,x;
    scanf("%d", &n);
    for(int i=1;i<=n;i++){
        scanf("%d", &x);
        if(Insert(lst,x,i)) continue;
        printf("第%d个输入不对哦\n",i);
        return 0;
    }
    scanf("%d", &m);
    int op,r;
    while(m--){
        printf("1 remove, 2 find 3 insert\n");
        scanf("%d%d", &op, &x);
        if(op==1){
            if(Remove(lst,x,r)) {
                printf("%d\n", r);
                continue;
            }
            printf("整错了\n");
        }
        else if(op==2){
            if(r=Find(lst,x)) printf("%d\n", r);
            else printf("找不到\n");
        }
        else{
            scanf("%d", &r);
            if(Insert(lst,x,r)) {
                continue;
            }
            printf("怎么想都进不去吧\n");
        }
    }
    return 0;
}

链表

线性链表是线性表的链接存储表示。元素之间的逻辑顺序是通过各结点中的链接指针来指示的。

分类 

单链表 循环链表 双向链表

链表中第一个元素结点称为首元结点,最后一个元素称为尾结点。首元结点不是头结点。

特点  每个元素(表项)由结点(Node)构成。

 线性结构  

结点可以连续,可以不连续存储  

结点的逻辑顺序与物理顺序可以不一致  

表可扩充

 在链表中,如果没有定义重载“++”函数,不能使用 p++ 这样的语句进到逻辑上的下一个结点。一般用 p = p->link 进到下一结点。

typedef char DataType;
typedef struct node{
    DataType data;
    struct node *link;
}LinkNode,*LinkList;
LinkList first;

插入的考虑  第一种情况:

        在第 1 个结点前插入:              

                newnode->link = first ;                  

                first = newnode;   

 第二种情况:

        在链表中间插入:       

        首先定位指针 p 到插入位置,再将新结点插在其后:          

                newnode->link = p->link;                   

                p->link = newnode;

 第三种情况:

        在链表末尾插入:       

        首先定义指针 p 到尾结点位置,再将新结点插在其后,新结点成为新的尾结点。                                   newnode->link = p->link;                 

                        p->link = newnode;

bool Insert(LinkList& first,int i,DataType x){  //在第i个节点插入新元素x
    //这个函数挺好,因为不会返回false且合情合理
    LinkNode *n;
    n=(LinkNode*)malloc(sizeof(LinkNode));
    n->data=x;
    if(first == NULL||i == 1){
        n->link=first;
        first=n;    //所以要引用型
    }
    else{
        LinkNode*p = first, *pr;
        int k=1;
        while(p!=NULL&&k<i-1){
            pr=p;
            p=p->link;
            k++;
        }
        if(p == NULL&&first != NULL) p = pr;
        //链太短,插在尾巴,p收缩到尾
        n->link=p->link;
        p->link=n;
    }
    return true;
}

 删除的考虑

        第一种情况:

         删除表中第 1 个元素

        第二种情况:

        删除表中或表尾元素

bool Remove(LinkList& first, int i, DataType& x){
    //在链表中删除第 i 个结点。如果要删除表中第 1 个结点,需要改变表头指针,所以 first 定义为引用型,被删元素的值通过引用型参数 x 返回
    LinkNode*p,*q;
    int k;
    if(i==0) return false;
    if(i==1){
        q=first;
        first=first->link;
    }
    else{
        p=first;
        int k=1;
        while(p!=NULL&&k<i-1){
            p=p->link;
            k++;
        }
        if(p==NULL||p->link==NULL){
            printf("无效删除位置\n");
            return false;
        }
        else{
            q=p->link;
            p->link=q->link;
        }
        x=q->data;
        free(q);  //删除*q
    }
    return true;
}

把你的链表拉出来溜溜~

void PrintList(LinkList first){
    while(first != NULL){
        printf("%d ", first->data);
        first=first->link;
    }
}

后插法建立单链表

每次将新结点加在插到链表的表尾;

设置一个尾指针 r,总是指向表中最后一个结点,新结点插在它的后面;

尾指针 r 初始时置为指向表头结点地址。

void insertRear(LinkList& first, DataType endTag){
    DataType val;
    LinkNode *s,*rear=first;   //rear指向表尾
    scanf("%d", &val);    //读入数据
    while(val!=endTag){
        s=(LinkNode*)malloc(sizeof(LinkNode));
        s->data=val;   //创建新节点并赋值
        rear->link=s;   //插入到表尾
        rear=s;       //读入下一数据
        scanf("%d", &val);
    }
    rear->link=NULL;    //表收尾
}

有序链表插入元素

void insertsort(LinkList& first,DataType endTag){  //有序单链表
    first=(LinkNode*)malloc(sizeof(LinkNode));
    first->link=NULL;
    DataType val;
    LinkNode *s,*rear=first;
    scanf("%d", &val);
    while(val!=endTag){
        s=(LinkNode*)malloc(sizeof(LinkNode));
        s->data=val;
        if(first->link==NULL){
            first->link=s;
            s->link=NULL;
            scanf("%d", &val);
            continue;
        }
        LinkNode *p=first->link;
        while(p&&p->data<val){
            rear=p;
            p=p->link;
        }
        rear->link=s;
        s->link=p;
        scanf("%d",&val);
    }
}

模拟集合求并

void merge(LinkList &L1,LinkList &L2,LinkList &L3){  //求并
    LinkList p1 = L1->link;
    LinkList p2 = L2->link;
    L1->link = NULL;
    L3 = L1;
    LinkList p3;
    while(p1&&p2){
        if(p1->data < p2->data){
            p3 = p1->link;
            p1->link=NULL;
            L1->link=p1;
            L1=L1->link;
            p1=p3;
        }
        else if(p1->data == p2->data){
            p3 = p1->link;
            p1->link = NULL;
            L1->link = p1;
            L1 = L1->link;
            p1 = p3;
            p2 = p2->link;
        }
        else{
            p3 = p2->link;
            p2->link = NULL;
            L1->link = p2;
            L1 = L1->link;
            p2 = p3;
        }
    }
    if(p2) p1=p2;
    while(p1){
        p3=p1->link;
        p1->link = NULL;
        L1->link = p1;
        L1 = L1->link;
        p1 = p3;
    }
    free(L2);
}

栈和队列

栈(Stack)

只允许在一端插入和删除的线性表。允许插入和删除的一端称为栈顶 (top),另一端称为栈底 (bottom)

特点:后进先出(LIFO)

栈的主要操作

        进栈 Push(S, x)

        退栈 Pop(S, &x)

        看栈顶 getTop(S, &x)

        置空栈 initStack(S)

        判栈空,判栈满


 几个基本函数

#define initSize 100
#define increament 20
typedef int SElemType;
typedef struct{  //顺序栈定义
    SElemType *elem;   //栈数
    int top,maxSize;   //栈顶指针及栈大小
}SeqStack;
void initStack (SeqStack& S){  //初始化
    S.elem = (SElemType*)malloc(initSize*sizeof(SElemType));
    if(S.elem == NULL){
        printf("failed to malloc!\n");
        exit(1);
    }
    S.top = -1;
    S.maxSize = initSize;
}
bool Empty(SeqStack& S){
    //判断栈是否空?空true,否则false
    return S.top == -1;
}
bool Full(SeqStack& S){
    //判断栈是否满,满返回true,否则false
    return S.top == S.maxSize-1;
}
void overFlow(SeqStack& S){  //栈满处理
    int newSize = S.maxSize + increament;
    // printf("BOOM!\n");
    SElemType *newS = (SElemType*)malloc(newSize*sizeof(SElemType)); //创建新数组
    for(int i=0;i<=S.top;i++) newS[i] = S.elem[i]; //向新数组传数据
    free(S.elem);   //释放老数组
    S.elem = newS;   //新数组作为栈数组
    S.maxSize = newSize;  //新数组大小
}   //栈指针不变
void Push(SeqStack& S,SElemType x){
    //若栈不满则新元素x进栈,否则扩大空间再进
    if(Full(S)) overFlow(S); //栈满溢出处理
    S.top++;
    S.elem[S.top] = x;   //加入新元素
}

bool Pop(SeqStack& S,SElemType& x){
    //若栈空返回false, 否则栈顶元素退出到x并返回true
    if(Empty(S)) return false;  //栈空返回false
    x = S.elem[S.top];  
    S.top--;                    //否则先取再退
    return true;
}
bool getTop(SeqStack& S, SElemType& x){
    //若栈空返回false, 否则栈顶元素读到x并返回true
    if(Empty(S)) return false;
    x = S.elem[S.top];
    return true;
}

 两个栈共享一个数组空间V[maxSize],设立栈顶指针数组 t[2] 和栈底指针数组 b[2]

t[i]和b[i]分别指示栈 i 的栈顶与栈底(i = 0, 1)

初始 t[0] = b[0] = -1,   t[1] = b[1] = maxSize  

栈满条件:t[0]+1 == t[1]              //栈顶指针相遇

栈空条件:t[0] = b[0] 或 t[1] = b[1]    //退到栈底

bool Push ( DualStack& DS, Type x, int i ) {
     if (DS.t[0]+1 == DS.t[1]) return false;
     if ( i == 0 ) DS.t[0]++; else DS.t[1]--;
     DS.V[DS.t[i]] = x;   
     return true;
}

bool Pop ( DualStack& DS, Type& x, int i ) {
     if ( DS.t[i] == DS.b[i] ) return false;
     x = DS.V[DS.t[i]]; 
     if (i == 0) DS.t[0]--; else DS.t[1]++;
     return true;
}

 顺序栈有栈满问题,一旦栈满需要做溢出处理,扩充栈空间,时间和空间开销较大。

链式栈无栈满问题,只要存储空间还有就可扩充。

链式栈的栈顶在链头,插入与删除仅在栈顶处执行。 链式栈适合于多栈操作,无需大量移动存储。

下述链式栈不带头结点。


typedef int SElemType;
typedef struct node{
    SElemType data;
    struct node *link;
}LinkNode, *LinkList, *LinkStack;
void initStack(LinkStack& S){  //栈初始化
    S = NULL;    //置空栈
}
bool stackEmpty(LinkStack& S){  //判空否
    return S == NULL;
}
bool Push(LinkStack& S, SElemType x){  //进栈
    LinkNode *p = (LinkNode*)malloc(sizeof(LinkNode));  //创建新节点
    if(p == NULL){
        printf("failed to build!\n");
        exit(1);
    }
    p->data = x;  //节点赋值
    p->link = S;
    S = p;    //链入栈顶
    return true;
}
bool Pop(LinkStack& S, SElemType& x){
    if(stackEmpty(S)) return false;
    LinkNode *p = S;
    x = p->data;
    S = p->link; //摘下原栈顶
    free(p);
    return true;
}

 栈的混洗

“混洗”原意是重新洗牌。用在本领域,问题的提法是:当进栈元素的编号为1, 2, …, n时,可能的出栈序列有多少种?

当进栈序列为1, 2时,可能的出栈序列有 2种:1, 2(进1出1进2出2)和 2, 1(进1进2出2出1);

当进栈序列为1, 2, 3时,可能的出栈序列有 5 种:

        1, 2, 3(进1 出1 进2 出2 进3 出3)

        1, 3, 2(进1 出1 进2 进3 出3 出2)

        2, 1, 3(进1 进2 出2 出1 进3 出3)

         2, 3, 1(进1 进2 出2 进3 出3 出1)

        3, 2, 1(进1 进2 进3 出3 出2 出1)

注意,3, 1, 2 是不可能的出栈序列,因为若 3 第1 个出栈,栈内一定是 1 压在 2 的下面,1 不可能先于 2 出栈。

一般情形如何呢?若设进栈序列为1, 2, …, n,可能的出栈序列有 m_{n} 种,

        则 n = 0时,m0 = 1: 出栈序列为{}。

        n = 1时,m1 = 1: 出栈序列为{1}。

        n = 2时,m2 = 2:

                出栈序列中 1 在首位,1 左侧有 0 个数,右侧有 1 个数,有 m0*m1 = 1 种

                        出栈序列:{1, 2}

                出栈序列中 1 在末位,1 左侧有 1 个数,右侧有 0 个数,有 m1*m0 = 1 种

                        出栈序列:{2, 1}。 可能出栈序列有 m0*m1+m1*m0 = 2种。

        n = 3时,m3 = 5:

                出栈序列中 1 在首位,1 左侧有 0 个数,右侧有 2 个数,有 m0*m2 = 2种                        

                        出栈序列:{1, 2, 3}和{1, 3, 2}。

                 出栈序列中1在中间, 1左侧有1个,右侧1个,m1*m1=1种;

                        出栈序列: {2,1,3}

                出栈序列中1在最后,左侧两个,右侧一个,m0*m2=2种

                        出栈序列: {3,2 ,1}和{2, 3, 1}

        n = 4,m4 = 14:

                出栈序列中 1 在第 1 位。1 左侧 0 个数,右侧 3 个数,有 m0*m3 = 5 种 出栈序列:

{1, 2, 3, 4}, {1, 2, 4, 3}, {1, 3, 2, 4}, {1, 3, 4, 2}, {1, 4, 3, 2}。

                出栈序列中 1 在第 2 位。1 左侧 1 个数,右侧 2 个数,有 m1*m2 = 2种 出栈序列:{2, 1, 3, 4}, {2, 1, 4, 3}。

                出栈序列中 1 在第 3 位。1 左侧 2 个数,右侧 1 个数,有 m2*m1 = 2种 出栈序列:{2, 3, 1, 4}, {3, 2, 1, 4}。

                出栈序列中 1 在第 4 位。1 左侧 3 个数,右侧 0 个数,有 m3*m0 = 5种 出栈序列:{2, 3, 4, 1}, {2, 4, 3, 1}, {3, 2, 4, 1}, {3, 4, 2, 1}, {4, 3, 2, 1}。

可能出栈序列 m0*m3 + m1*m2 + m2*m1 + m3*m0  = 5+2+2+5 = 14 种。 

一般地,有 n 个元素按序号1, 2, …, n 进栈,轮流让 1 在出栈序列的第 1, 第 2, …第 n 位,则可能的出栈序列数为:

队列

 定义

        队列是只允许在一端删除,在另一端插入的线性表

        允许删除的一端叫做队头 (front),允许插入的一端叫做队尾 (rear)。

特性

        先进先出 (FIFO, First In First Out)

 队列与栈的共性在于它们都是限制了存取位置的线性表;区别在于存取位置有所不同。

 队列的进队和出队的原则

有两种进/出队列的方案:

        先加元素再动指针

        进队时先将新元素按 rear 指示位置加入,再让队尾指针进一  rear = rear + 1。                

        队尾指针指示实际队尾的后一位置。

        出队时先将下标为 front 的元素取出,再将队头指针进一 front = front + 1。

        队头指针指示实际队头的位置。 清华、北大教材均为此方案

        先动指针再加元素

        进队时先让队尾指针进一  rear = rear + 1,再将新元素按 rear 指示位置加入。

        队尾指针指示实际队尾的位置。

        出队时先将队头指针进一 front = front + 1,再将下标为 front 的元素取出。

        队头指针指示实际队头的前一位置。

        微软Visual C++ STL 按此处理。

队满时再进队出现的溢出往往是假溢出,即还有空间但用不上,为了有效利用队列空间,可将队列元素存放数组首尾相接,形成循环队列。

循环队列(Circular Queue)

队列存放数组被当作首尾相接的环形表处理。

队头、队尾指针加 1 时从 queSize-1 直接进到0,可用语言的取模(%)运算实现。

        队头指针进1:  front = (front+1) % queSize;

        队尾指针进1:  rear = (rear+1) % queSize;

        队列初始化:front = rear = 0;

        队空条件:front == rear;

        队满条件:(rear+1) % queSize == front。 注意,进队和出队时指针都是顺时针前进

void initQueue ( CircQueue& Q ) {       //置空队列
     Q.rear = 0;  Q.front = 0;
}

bool queueEmpty ( CircQueue& Q ) {   //判队空否
     return Q.rear == Q.front;
}

bool queueFull ( CircQueue& Q ) {       //判队满否
     return (Q.rear+1) % queSize == Q.front;
}

 //当进队速度快于出队速度,rear追上front,造成了队满,为了区分队空条件,认定当rear+1 == front 时队列已满。这样不必增加其他辅助单元。

bool enQueue ( CircQueue& Q, QElemType x ) {
//在循环队列Q的队尾加入新元素 x
     if ( queueFull(Q) ) return false;      //队已满
     Q.elem[Q.rear] = x;			 //否则,先加
     Q.rear = (Q.rear+1) % queSize;       //队尾指针进一
     return true;
}
bool deQueue ( CircQueue& Q, QElemType& x ) {
     if ( queueEmpty(Q) ) return false;		
     x = Q.elem[Q.front];			//先取队头的值
     Q.front = (Q.front+1) % queSize;	//队头指针进一
     return true;
}

bool getFront ( CircQueue& Q, QElemType& x ) {
     if ( queueEmpty(Q) ) return false;
     x = Q.elem[Q.front];  return true;
}

链式队列

 链式队列采用不带头结点的单链表存储队列元素,队头在链头,队尾在链尾。

链式队列在进队时无队满问题,但有队空问题。

队空条件为 front->link == NULL,不必判断是否 rear == front。

链式队列特别适合多个队列同时操作的情形。在并行处理、排序等方面有用。

链式队列的结构定义

typedef int QElemType;      //元素的数据类型

typedef struct node {	
     QElemType data;	     //队列结点数据
     struct node *link;            //结点链指针
} LinkNode;

typedef struct {		    //队列结构定义
    LinkNode *rear, *front;   //队尾与队头指针
} LinkQueue;
void initQueue ( LinkQueue& Q ) {
     Q.front = NULL;  Q.rear = NULL;  //置空队列
}

bool queueEmpty ( LinkQueue& Q ) {
      return Q.front == NULL;               //判队空否
}

bool getFront ( LinkQueue& Q, QElemType& x ) {
      if ( queueEmpty(Q) ) return false;		
      x = Q.front->data;  return true;     //读取队头元素
}
bool enQueue ( LinkQueue& Q, QElemType x ) {
    LinkNode *s = (LinkNode *) malloc ( sizeof  
          (LinkNode));   		   //创建新队尾结点
	s->data = x;  s->link = NULL;
    if ( Q.rear == NULL ) 		   //新结点加入空队
        { Q.rear = s;  Q.front = s; }
    else 		    		   //新结点成新链尾
        { Q.rear->link = s;  Q.rear = s; }
    return true;	
}
bool deQueue ( LinkQueue& Q, QElemType& x ) {
//删去队头结点,并返回队头元素的值
     if ( queueEmpty(Q) ) return false;	//队空不能删
     LinkNode *p = Q.front;		
     x = p->data;                           	           //保存队头的值
     Q.front = p->link;   		           //新队头
 	 if ( Q.front == NULL ) Q.rear = NULL;
	        //若删除后队空,队尾指针置为空
     free (p);  return true;				
}

队列的应用:打印杨辉三角形

//#include “Linkqueue.cpp"
void Yanghui ( int n ) {
	 LinkQueue Q;                             //创建队列Q
     InitQueue (Q);
	 EnQueue(Q, 1);  EnQueue(Q, 1); //第 1 行系数进队 
     int s = 0, t;
     for (int i = 1; i <= n; i++ ) {             //逐行输出
         printf (“\n”);					
         EnQueue (Q, 0);					
         for ( int j = 1; j <= i+2; j++ ) {     //下一行
		     DeQueue(Q, t);   EnQueue(Q, s+t);
			 //计算下一行系数, 并进队列 		
                s = t;
                if ( j != i+2 ) printf (“%d”, s);	 //输出一个系数
          } 
     }
}

 栈的应用:括号匹配

若用字符串描述的表达式为“(a+(b-c)*d)+e/f”,设置一个栈 S 用于判断括号是否配对。然后从左向右扫描表达式中的每一个字符:

        当遇到左括号“(”,进栈,扫描下一字符;

        当遇到右括号“)”,判断栈是否空? 若栈空,则“)”多于“(”,报错;

        若栈不空,则退栈顶的“(”,扫描下一字符;

        当遇到的不是括号,扫描下一字符;

        若表达式扫描完,栈不空,则“(”多于“)”,报错。

int BracketsCheck ( char e[ ], int n ) {
//对字符数组e[n]中的表达式进行括号配对检查。
//若括号匹配,函数返回1,否则函数返回0。
    SeqStack S;  initStack(S);	//定义一个栈
	for ( int i = 0; i < n; i++ ) 	//顺序扫描e[n]中字符 
        if ( e[i] == '{' || e[i] == '[' || e[i] == '(' ) 
            Push ( S, e[i] ); 	          //左括号进栈
	    else if ( e[i] == '}' ) {
	        if ( stackEmpty(S) ) 
                { printf ( " '{'比'}'少!\n" ); return 0; }
	        if ( getTop(S) != '{' ) {
                printf ( "%c与'}'不配对!\n", getTop(S) );
                return 0;
            }
            Pop(S);	 		//花括号配对出栈
        }
        else if ( e[i] == ']' ) {
            if ( stackEmpty(S) ) 
                { printf ("缺'['!\n"); return 0; }	
            if ( getTop(S) != '[' ) {
                printf ("%c与']'不配对!\n", getTop(S) );
                return 0;
            }  
              Pop(S);   			//方括号配对出栈
        }
        else if ( e[i] == ')' ) {
            if ( stackEmpty(S) )
                 { printf ("缺'('!\n"); return 0; }	
            if ( getTop(S) != '(' ) {
                printf ("%c与')'不配对!\n ", getTop(S));
                return 0;
            }
            Pop(S);			//圆括号配对出栈
        }
    if ( stackEmpty(S) )               //表达式扫描完且栈空
 	    { printf ("括号配对!\n");  return 1; }
    else { 
        while ( ! stackEmpty(S) )  
            if ( getTop(S) == '{' ) 
                { printf ("缺'}' ");  Pop(S); }
            else if ( getTop(S) == '[' ) 
                { printf ("缺']' ");  Pop(S); }
            else if ( getTop(S) == '(' )
                { printf ("缺')' ");  Pop(S); }	
        printf ("\n");  return 0;
    }
}

栈的应用:表达式求值

        表达式求值是一种典型的栈的应用。

         一个表达式由操作数(亦称运算对象)、操作符 (亦称运算符) 和分界符组成。

        算术表达式有三种表示:

                中缀(infix)表示     <操作数> <操作符> <操作数>,如 A+B;

                前缀(prefix)表示     <操作符> <操作数> <操作数>,如 +AB;

                后缀(postfix)表示      <操作数> <操作数> <操作符>,如 AB+;

       例如,

        中缀表达式    a + b * ( c - d ) - e / f

        后缀表达式    a b c d - * + e f / -

        表达式中相邻两个操作符的计算次序为:

                优先级高的先计算;

                优先级相同的自左向右计算;

                当使用括号时从最内层括号开始计算。

        当使用中缀表达式计算时,需要同时使用两个栈辅助求值;而使用后缀表达式求值,则只需要一个栈,相对简单一些。

应用后缀表示计算表达式的值

        从左向右顺序地扫描表达式,并用一个栈暂存扫描到的操作数或计算结果。

        在后缀表达式的计算顺序中已隐含了加括号的优先次序,括号在后缀表达式中不出现。

 一般表达式的操作符有 4 种类型:

        算术操作符   如双目操作符(+、-、*、/ 和%)以及单目操作符(-)。

        关系操作符   包括<、<=、==、!=、>=、>。这些操作符主要用于比较。

        逻辑操作符   如与(&&)、或(||)、非(!)。

        括号‘(’和‘)’   它们的作用是改变运算顺序。

通过后缀表示计算表达式值的过程

        顺序扫描表达式的每一项,根据它的类型做如下相应操作:

                若该项是操作数,则将其压栈

                若该项是操作符<op>,则连续从栈中退出两个操作数Y和X,形成运算指令X<op>Y,并将计算结果重新压栈

        当表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。

        举例  a b c d - * + e f ^ g / -

利用栈将中缀表示转换为后缀表示

使用栈可将表达式的中缀表示转换成它的前缀表示和后缀表示。

为了实现这种转换,需要考虑各操作符的优先级。

C/C++中操作符的优先级

 各个算术操作符的优先级

 isp 叫做栈内 (in stack priority) 优先数

icp 叫做栈外 (in coming priority) 优先数。

操作符优先数相等的情况只出现在括号配对栈底的“#”号与输入流最后的“#”号配对时。

在把中缀表达式转换为后缀表达式的过程中,需要检查算术运算符的优先级,以实现运算规则。

中缀表达式转换为后缀表达式

        操作符栈初始化,将结束符 ‘#’ 进栈。然后读入中缀表达式字符流的首字符 ch。

        重复执行以下步骤,直到 ch = ‘#’,同时栈顶的操作符也是‘#’,停止循环。

                若ch是操作数直接输出,读入下一个字符 ch。

                若 ch 是操作符,判断 ch 的优先级 icp 和位于栈顶的操作符op的优先级 isp:

                        若 icp (ch) > isp (op),令ch进栈,读入下一个字符ch。(看后面是否有更高的)

                        若 icp (ch) < isp (op),退栈并输出。     (执行先前保存在栈内的优先级高的操作符

                        若 icp (ch) == isp (op),退栈但不输出,若退出的是“(”号读入下一个字符ch。(销括号)

         算法结束, 输出序列即为所需的后缀表达式。

举例,将中缀表达式

              a + b * (c - d) – e / f #     

转换为后缀表达式

            a b c d - * + e f / -

 应用中缀表示计算表达式的值

使用两个栈,操作符栈OPTR (operator),操作数栈OPND(operand)

为了实现这种计算,仍需要考虑各操作符的优先级,参看前面给出的优先级表。

 中缀算术表达式求值

 对中缀表达式求值的一般规则:

        建立并初始化OPTR栈和OPND栈,然后在OPTR栈中压入一个“#”

        扫描中缀表达式,取一字符送入ch。

        当ch != ‘#’ OPTR栈的栈顶 != ‘#’时, 执行以下工作, 否则结束算法。在OPND栈的栈顶得到运算结果。

                若ch是操作数,进OPND栈,从中缀表达式取下一字符送入ch;

                 若ch是操作符,比较icp(ch)的优先级和isp(OPTR)的优先级:

                        若icp(ch) > isp(OPTR),则ch进OPTR栈,从中缀表达式取下一字符送入ch;

                        若icp(ch) < isp(OPTR),则从OPND栈退出a2和a1,从OPTR栈退出θ, 形成运算指令 (a1)θ(a2),结果进OPND栈;

                        若icp(ch) == isp(OPTR) 且ch == ')',则从OPTR栈退出'(',对消括号,然后从中缀表达式取下一字符送入ch;

 栈的应用:递归

 递归的定义     

        若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的;若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。

 以下三种情况常常用到递归方法。

        定义是递归的

        数据结构是递归的

        问题的解法是递归的

 例如,阶乘函数(Factorial)

long Factorial ( long n ) {
    	if (n < 0) exit(0);
    	if (n == 0) return 1;		//递归终止
    	else return n*Factorial (n-1);	//减一递归
	}

 例如,单链表结构

单链表中一个结点,它的指针域为NULL,其后继是一个链表,是空链表;  

单链表中一个结点,它的指针域非空,其后继仍是一个单链表,是非空单链表。

从结构上,用指针定义结点,又用结点定义指针。 

 链表的递归结构定义

	typedef struct node {          //单链表定义
    	ElemType data;            
    	struct node *link;         //定义结点用到指针
	} LinkNode,  *LinkList;      //定义指针用到结点

基于递归定义的数据结构,相应算法的实现均可采用递归方式。下面举例。

在以 f 为表头指针的不带头结点的单链表中正向打印所有结点所存储的数值。

     void printValue ( LinkNode *f ) {
        if ( f != NULL) {		 //递归结束条件
		    printf ( f ->data );	 //打印当前结点的值
	      	    PrintValue ( f->link );   //递归打印后续链表
	    }
    }

 在以 f 为表头指针的不带头结点的单链表中反向打印所有结点所存储的数值。

    void printValue ( LinkNode *f ) {
	if ( f != NULL) {		   //递归结束条件
		   PrintValue ( f ->link );     //递归打印后续链表
		   printf ( f ->data );	   //返回后打印结点值
	      }
    }

 问题的解法是递归的

 例如,汉诺塔 (Tower of Hanoi) 问题的解法:

        如果 n = 1,则将这一个盘子直接从 A 柱移到 C 柱上。否则,执行以下三步:

                用 C 柱做过渡,将 A 柱上的(n-1) 个盘子移到 B 柱上:

                将 A 柱上最后一个盘子直接移到 C 柱上;

                用 A 柱做过渡,将 B 柱上的(n-1) 个盘子移到 C 柱上。

这是典型的分治法问题。

#include <stdlib.h>
//#include "string.h”
void Hanoi ( int n, char A, char B, char C ) {
 //用A、B、C代表三个柱子,算法模拟汉诺塔问题
     if (n == 1) printf ( " move %s", A, " to  %s ", C );   
     else {  
		Hanoi ( n-1, A, C, B );
	      	printf ( " move %s", A, " to  %s ", C );
		Hanoi ( n-1, B, A, C );
     }
}

 递归过程与递归工作栈

 递归过程在实现时,需要自己调用自己。

层层向下递归,退出时的次序正好相反:

递归调用

 返回次序

 主程序第一次调用递归过程为外部调用;递归过程每次递归调用自己为内部调用。它们返回调用它的过程的地址不同。

每次调用必须记下返回上层什么地方的地址。

递归工作栈

每一次递归调用,需要为过程中使用的参数、局部变量等另外分配存储空间,每个过程的工作空间互不干扰,回到上层还可恢复上层原来的值。

每层递归调用需分配的空间形成递归的工作记录,按后进先出的栈组织。  

 计算Fact时活动记录的内容

 递归过程改为非递归过程

 递归过程简洁、易编、易懂。然而,递归过程效率低,重复计算多

例如,定义一个计算斐波那契数列的递归函数

 如 F0 = 0, F1 = 1, F2 = 1, F3 = 2, F4 = 3, F5 = 5, F6 = 8, …

 求解斐波那契数列的递归算法为:

	long Fib(long n) {      
        if ( n <= 1 ) return n;		
        else return Fib(n-1) + Fib(n-2);
  	}

 递归算法的缺点是重复计算多。从计算斐波那契数列的例子可知,递归调用次数可达         NumCall(k) = 2*Fk+1 - 1

例如计算Fib(5),Fib(5)计算 1 次,Fib(4)计算 1 次,Fib(3)计算 2 次,Fib(2)计算 3 次,Fib(1)计算 5 次,Fib(0)计算 3 次。计算次数 = 1 + 1 + 2 + 3 + 5 + 3 = NumCall(5) = 2*F6-1 = 15。

计算Fib(5)的递归调用树为:

 递归深度达到 5。

为提高算法的计算效率,可以改递归过程为非递归过程。

 尾递归单向递归可直接用迭代实现其非递归过程,其他情形必须借助栈实现非递归过程。

尾递归用迭代法实现

例如,一个求阶乘的函数:

	long Factorial ( long n ) {
    	if ( n <= 1 ) return 1;
     	else return n*Factorial (n-1);
	}

这是典型的尾递归。

程序内只有一个递归语句,且位于程序最后。它不再需要使用返回地址(反正回到上一层的最后),也不需继续使用局部变量。

递归函数传递的参数可以作为循环变量,从而把尾递归改为循环,加快了算法的执行速度。

求阶乘的非递归算法如下所示。

	long Fact ( long n ) {  	    //尾递归改为迭代算法
    	long f = 1;
	       for ( int i = 2; i <= n; i++ ) f = i*f;
           return f; 
	}

求阶乘的递归算法称为“减治”法,它通过递归逐步降低问题的规模,直到能直接求解。

单向递归用迭代法实现

单向递归是指递归过程执行时虽然可能有多个分支,但可以保存前面计算的结果以供后面的语句使用。

例如计算斐波那契数列的递归算法,只要保存前两次计算的结果,就可以执行后续的计算。无需使用栈来保存递归工作记录。

用迭代法实现斐波那契数列的计算

long FibIter ( long n ) {
      if ( n <= 1 ) return n;
      long a = 0,  b = 1,  c;
      for ( int i = 2;  i <= n; i++ ) {
           c = a+b;             	//求 Fi = Fi-2 + Fi-1
           a = b;   b = c;     	//下一个 Fi-2 = 原来的 Fi-1
	  }                            	//下一个 Fi-1 = 原来的 Fi
	  return c;
}

递归问题的非递归算法编写技巧

递归工作栈

        每一次递归调用时,需要为过程中使用的参数、局部变量等另外分配存储空间。

        每层递归调用需分配的空间形成递归工作记录,按后进先出的栈组织。

确定入栈的返回信息,这些返回信息在出栈时可以帮助计算函数值或者新的返回信息

确定入栈的条件,即非递归终点的条件

确定出栈时对栈顶的操作,是替换成新的返回信息还是直接出栈

确定算法终止的条件

递归问题的非递归算法编写技巧

用栈将递归算法改为非递归算法

以汉诺塔(Hanoi)为例,它有两个内部递归调用的语句,不属于单向递归和尾递归,必须利用栈记录调用状态。

定义栈元素的数据类型

	typedef struct {
		int m;  char a, b, c;
	} item;

求解hanoi塔问题

#define stackSize 100
	void Hanoi ( int n, char A, char B, char C ) {
	    item v, w, S[stackSize];   int top = -1;   
        w.m = n;   w.a = A;   w.b = B;   w.c = C;
	    S[++top] = w;		//初始布局进栈
        while ( top != -1 ) {        //当栈非空时
            v = S[top--];	        //取栈顶布局,退栈
	        if ( v.m == 1 ) printf (“Move disk from peg 
                    %c to %c. \n”, v.a, v.c );       //直接搬动
	        else {
	            w.a = v.b;  w.b = v.a;  w.c = v.c;
                w.m = v.m-1;  S[++top] = w;   //(n-1,B,A,C)
                w.a = v.a;  w.b = v.b;  w.c = v.c;
                w.m = 1;  S[++top] = w;	     //(1,A,B,C)
		      w.a = v.a;  w.b = v.c;  w.c = v.b;
                w.m = v.m-1;  S[++top] = w;  //(n-1,A,C,B)
            }  //end else
        }   //end while
    }     //Hanoi

递归与分治法

在使用分而治之策略(即分治法)解决复杂问题时常用的方法即递归的方法。

例如解决汉诺塔问题就属于分治法。所谓分治法就是在解决一个规模比较大的问题时,首先研究问题的结构,把它分解为一个或几个规模比较小的同类型问题,分别对这些比较小的问题求解,再综合它们的结果,从而得到原问题的解。这些比较小的问题的求解方法与原来问题的求解方法一样。

把复杂化为简单,是分治法的精髓。

递归与回溯法

对一个包含有许多结点,且每个结点有多个分支的问题,可以先选择一个分支进行搜索。当搜索到某一结点,发现无法再继续搜索下去时,可以沿搜索路径回退到前一结点,沿另一分支继续搜索。如果回退之后没有其他选择,再沿搜索路径回退到更前结点,…。(联想深搜)

简化的迷宫问题  

例,一个有 7 个路口     的小型迷宫如图。       路口 1 是入口,路口       7 是出口。所有路口          的前进方向可以用一     个前进方向表表示。

   

路口数据 0 表示该方向堵塞不能前进; 向前试探顺序是左行、直行、右行; 前进遇到堵塞则回溯,遇到出口则试探成功。

 

 迷宫的结构定义

struct Maze {
     int MazeSize;	                  //迷宫大小(路口数)
     int EXIT;			        //出口号码       
     Intersection *intsec;   	        //路口数组
}

 前进路口结构定义

struct Intersection {
     int left, forwd, right;    	        //路口信息
}
bool Traverse(int Pos) {       	//迷宫漫游算法
	  if (Pos > 0) { 	                	//路口从 1 开始
      	if (Pos == EXIT)                               	  //出口
      	      { printf (Pos << "  ");  return 1; }
		else if ( Traverse(intsec[Pos].left) )     	  //向左
                { printf (Pos << "  ");  return 1; }
          else if (Traverse(intsec[Pos].forwd)) 	  //向前
                { printf (Pos << "   ");  return 1; }
          else if (Traverse(intsec[Pos].right))   	  //向右 
                { printf (Pos << "   ");  return 1;  }
	}
return 0;
}

 双端队列

双端队列(Deque)是对队列的扩展,它允许在队列的两端进行插入和删除。双端队列英文的全称是Double-ended queue。

我们可以把双端队列视为底靠底的双栈,但它们相通,成为双向队列。两端都可能是队头和队尾

 一般地,若有 n 个元素,进入双端队列的顺序是1, 2, …, n(不管是何端进/出),可用数学归纳法证明:全进全出后可能的出队顺序有 n! 种。即可输出全排列

而普通的先进先出队列的可能的出队顺序仅有 1种。

假设有 2 个整数 1, 2 顺序进入双端队列,再退出双端队列,则可能的出队序列 2 种,即 1, 2 和 2, 1。因为双端队列可视为底靠底的双栈,也可视为普通的队列。如果输入序列是 1, 2, 3,则 3 可以放在 1, 2 前面,也可以放在 1, 2 中间,还可放在 1, 2 后面,有 3!种输出顺序。

 输入受限的双端队列

如果限定只能在双端队列的一端输入,可以在两端输出,那么对于一个确定的输入序列,输出只能有 3 种可能:在同一端输出,相当于栈;或者在另一端输出,相当于队列;或者混合进出。

 假设输入整数是 1, 2, 3,同一端输入/输出,有 5 种(相当于栈),即1 2 3/1 3 2/2 1 3/2 3 1/3 2 1。还剩 3 1 2,它也是合理的输出序列:当1, 2, 3顺序入队之后,3 在同一端出队(相当于栈),1在另一端出队(相当于队列),在最后 2 出队。  

如果1, 2, 3, 4 顺序入队,都在同一端出队(相当于栈)有 14 种出队顺序,除此之外还有 4! – 14 = 10 种可能的出队序列。

实际上,不合理的出队序列基本上都是以 4 打头的。当 4 先出队时,1, 2, 3 依次排在队列里,2 夹在中间,它不可能在 1 和 3 之前出队,所以不可能的出队序列只有 4 2 3 1和4 2 1 3

输出受限的双端队列

这种双端队列限定只能在队列的一端输出,但可以在两端输入,对于一个确定的输入序列,输出顺序也有 3 种可能:在同一端输入和输出,相当于栈;或者在一端输入一端输出,相当于队列;或者混合进出。

假设输入整数是 1, 2, 3,同一端输入/输出,有 5 种出队序列,还剩 3, 1, 2。如果限定在左端允许输出,可以在任意端先让 1 入队,再让 2 从右端入队,3 从左端入队,这样 3, 1, 2 也是合理的出队序列。

当输入整数是 1, 2, 3, 4 时,同样先排除允许在同一端输入/输出的14种情况,不可能的输出序列还是要在以 4 开头的排列中查找。

在以 4 开头的排列中,有问题的是 4, 1, 2, 3/4, 1, 3, 2/4, 3, 1, 2/4, 2, 1, 3/4, 2, 3, 1。

对于4, 1, 2, 3,先从右端输入1, 2, 3,再从左端输入4,即可从左端输出4, 1, 2, 3。

对于4, 1, 3, 2,必须最后从左端输入 4,在此之前在队列中需得到1, 3, 2的排列,这是不可能的。

对于4, 3, 1, 2,先从右端输入1, 2,再从左端输入3, 4,即可从左端输出4, 3, 1, 2。

对于4, 2, 1, 3, 先从左端输入1, 2,再从右端输入3,最后从左端输入4,即可从左端输出4, 2, 1, 3。

对于4, 2, 3, 1,同样在 4 输入前,在队列中得不到 2, 3, 1 这种排列。3 不可能夹在 1 和 2 中间,所以这是不可能的输出序列。

最后可知,4, 1, 3, 2 和 4, 2, 3, 1 是不可能的出队序列。问题出在 3 不可能在1, 2之间进队

优先队列 (Priority Queue)

优先队列   每次从队列中取出的是具有最高优先权的元素

如下表:任务优先权及执行顺序的关系

数字越小,优先权越高

    #define maxPQSize 50
    typedef int PQElemType;
    typedef struct {
        PQElemType elem[maxPQSize];  //存放数组
        int n;	                                          //当前元素计数
    } PQueue;

每次在优先队列中插入新元素时,新元素总是插入在队尾;

在优先队列中每次从队列中查找权值最小的元素删除,再把队列最后元素填补到被删元素位置。

bool PQInsert ( PQueue& Q, PQElemType x ) {
    if ( Q.n == maxPQSize ) return false;   //队满不插入
    Q.elem[Q.n++] = x;			    //在队尾插入
    return true;
}

bool PQRemove ( PQueue& Q, PQElemType& x ) {
    if ( Q.n == 0 ) return flase;		//队空不能删除 
    PQElemType min = Q.elem[0];  int k = 0;
    for ( int i = 1; i < Q.n; i++ )		//查找最小值
        if ( Q.elem[i] < min ) { min = Q.elem[i];  k = i; }
    x = Q.elem[k];  Q.n--;
    Q.elem[k] = Q.elem[Q.n];  return true;
}
//作业:一个自认为很完美的中缀表达式求值(包括+-*/()无空格)
#include<bits/stdc++.h>
using namespace std;
typedef double OPNDtype;
typedef char OPTRtype;
typedef struct node1{
    OPNDtype data;
    struct node1 *link;
}OPNDnode, *OPNDStack;
typedef struct node2{
    OPTRtype data;
    struct node2 *link;
}OPTRnode, *OPTRStack;
void initOPND(OPNDStack& S){  //栈初始化
    S = NULL;    //置空栈
}
void initOPTR(OPTRStack& S){
    S = NULL;
}
bool OPNDEmpty(OPNDStack& S){  //判空否
    return S == NULL;
}
bool OPTREmpty(OPTRStack& S){
    return S == NULL;
}
bool PushOPND(OPNDStack& S, OPNDtype x){  //进栈
    OPNDnode *p = (OPNDnode*)malloc(sizeof(OPNDnode));  //创建新节点
    if(p == NULL){
        printf("failed to build!\n");
        exit(1);
    }
    p->data = x;  //节点赋值
    p->link = S;
    S = p;    //链入栈顶
    return true;
}
bool PushOPTR(OPTRStack& S, OPTRtype x){
    OPTRnode *p = (OPTRnode*)malloc(sizeof(OPTRnode));
    if(p == NULL){
        printf("failed to build!\n");
        exit(1);
    }
    p->data = x;
    p->link = S;
    S = p;
    return true;
}
bool PopOPND(OPNDStack& S, OPNDtype& x){
    if(OPNDEmpty(S)) return false;
    OPNDnode *p = S;
    x = p->data;
    S = p->link; //摘下原栈顶
    free(p);
    return true;
}
bool PopOPTR(OPTRStack& S, OPTRtype& x){
    if(OPTREmpty(S)) return false;
    OPTRnode *p = S;
    x = p->data;
    S = p->link;
    free(p);
    return true;
}
bool TopOPND(OPNDStack& S, OPNDtype& x){
    if(OPNDEmpty(S)) return false;
    x = S->data;
    return true;
}
bool TopOPTR(OPTRStack& S, OPTRtype& x){
    if(OPTREmpty(S)) return false;
    x = S->data;
    return true;
}
string s;
string format(string str){    //部分负数补0
    for(int i = 0;i < str.length();i++){
        if(str[i] == '-'){
            if(i == 0) str.insert(0,1,'0');
            else if(str[i-1] == '('){
                str.insert(i,1,'0');
            }
        }
    }
    return str;
}
int priorisp(char x){
    if(x == '#') return 0;
    if(x == '(') return 1;
    if(x == '^') return 7;
    if(x == '*'||x == '/') return 5;
    if(x == '+'||x == '-') return 3;
    if(x == ')') return 8;
    printf("OPTR error\n");
    return 0;
}
int prioricp(char x){
    if(x == '#') return 0;
    if(x == '(') return 8;
    if(x == '^') return 6;
    if(x == '*'||x == '/') return 4;
    if(x == '+'||x == '-') return 2;
    if(x == ')') return 1;
    printf("OPTR error\n");
    return 0;
}
double readD(string str, int& m)
{
    int zh = 0;//整数部分
    int w = 1;//符号
    bool flag = false;//小数点
    double xi = 0.0;//小数部分
    double x = 1.0;//位
    int i=1;
    char ch = str[0];//开读
    while (ch >= '0' && ch <= '9')//整数部分
    {
        zh = zh * 10 + ch - '0';
        ch = str[i++];
        m++;
    }
    while (ch < '0' || ch > '9')//找小数点
    {
        if (ch == '.') {flag = true;m++;}
        ch = str[i++];
    }
    if (!flag) return zh;//没有返回整数
    while (ch >= '0' && ch <= '9')//小数部分
    {
        x *= 10;
        xi = xi + (ch - '0') / x;
     	ch = str[i++];
        m++;
    }
    if (w == 1) return zh + xi;
    else return zh - xi;
}
bool analysis(string str){   //表达式合法?(一部分判断)
    for(int i=0;i<str.length();i++){
        if(str[i] == '('||str[i] == ')'||str[i] == '+'||str[i] == '-'||str[i] == '*'||str[i] == '/'){
            continue;
        }
        else if(str[i]>='0'&&str[i]<='9'){
            string tmp;
            while(str[i]>='0'&&str[i]<='9'){
                tmp.push_back(str[i]);
                i++;
            }
            if(str[i] == '.'){
                i++;
                if(str[i]>='0'&&str[i]<='9'){
                    tmp.push_back('.');
                    while(str[i]>='0'&&str[i]<='9'){
                        tmp.push_back(str[i]);
                        i++;
                    }
                }
                else{
                    return false;  //.后下一符号前不是数则判错
                }
            }
            // word.push_back(make_pair(tmp, 5));
            i--;
        }
        else{
            return false; //.开头则判错
        }
    }
    return true;
}
int main(){
    OPNDStack nums;
    OPTRStack ops;
    initOPND(nums);
    initOPTR(ops);
    PushOPTR(ops, '#');
    getline(cin, s);
    if(!analysis(s)){
        printf("error!(points, spaces...(初步判断不合法))\n");
        return 0;
    }
    s=format(s)+'#';
    int i=0;
    while(i<s.length()){
        if(s[i]>='0'&&s[i]<='9'){
            double n;
            int m = 0;
            n = readD(s.substr(i,s.length()), m);
            PushOPND(nums, n);
            i += m;
            if(i>=s.length()) break;
        }
        else if(s[i] == '+'||s[i] == '-'||s[i] == '*'||s[i] == '/'||s[i] == '('||s[i] == ')'||s[i] == '#'){
            char ot=(char)s[i];
            char it, res;
            TopOPTR(ops, it);
            if(prioricp(ot)>priorisp(it)) {PushOPTR(ops, ot);i++;}
            else if(prioricp(ot)<priorisp(it)) {
                char a;
                if(OPTREmpty(ops)) {printf("Error!!!(缺少操作符)\n"); return 0;}
                PopOPTR(ops, a);
                double b;
                if(OPNDEmpty(nums)) {printf("ERror!!(缺少操作数)\n"); return 0;}
                PopOPND(nums, b);
                double c;
                if(OPNDEmpty(nums)) {printf("ERRor!!!(缺少操作数)\n"); return 0;}
                PopOPND(nums, c);
                if(a == '+') PushOPND(nums, c+b);
                else if(a == '-') PushOPND(nums, c-b);
                else if(a == '*') PushOPND(nums, c*b);
                else if(a == '/') PushOPND(nums, c/b);
            }
            else {PopOPTR(ops, res);i++;}
        }
        else {i++;continue;}
    }
    if(OPNDEmpty(nums)) {
        printf("ERROR!!!(操作数栈为空)\n");
        return 0;
    }
    double ans;
    PopOPND(nums, ans);
    if(OPNDEmpty(nums)){
        printf("%.2lf\n", ans);
    }
    else printf("ERROR!!!(操作数栈剩余两个数及以上)\n");
    return 0;
}

字符串

字符串的概念

字符串是 n ( >= 0 ) 个字符的有限序列,

    记作   S : “c1c2c3…cn”

    其中,S 是串名字

                “c1c2c3…cn”是串值

                ci 是串中字符

                n 是串的长度,n = 0 称为空串。

例如,  S = “Tsinghua University”。

注意:空串和空白串不同,例如 “  ” 和 “” 分别表示长度为1的空白串和长度为0的空串。

串中任意个连续字符组成的子序列称为该串的子串,包含子串的串相应地称为主串。

通常将子串在主串中首次出现时,该子串首字符对应的主串中的序号,定义为子串在主串中的位置。

例如,设A和B分别为

        A = “This is a string”   B = “is”

    则 B 是 A 的子串,A 为主串。B 在 A 中出现了两次,首次出现所对应的主串位置是2(从0开始)。因此,称 B 在 A 中的位置为2。

特别地,空串是任意串的子串,任意串是其自身的子串

通常在程序中使用的串可分为两种:串变量和串常量。

串常量在程序中只能被引用但不能改变它的值,即只能读不能写。通常串常量是由直接量来表示的,例如语句 Error (“overflow”) 中“overflow”是直接量。但有的语言允许对串常量命名,以使程序易读、易写。如C中可定义

         char path[] = “dir/bin/appl”;

这里path存储的是一个串常量。串变量和其它类型的变量一样,其取值可以改变。

在C中常用的字符串操作

字符串初始化

 char name[12]  = "Tsinghua";
 char name[  ] = "Tsinghua";
 char name[12] = {'T','s','i','n','g','h','u','a'};
 char name[  ] = {'T','s','i','n','g','h','u','a','\0'};
 char *name = "Tsinghua";
 char name[12];

name = “Tsinghua”;   错误  因数组名是地址常量

这些都会截图偷个懒 

字符串的实现

除 C 语言提供的字符串库函数外,可以自定义字符串。适用于自定义字符串数据类型的有三种存储表示:定长顺序存储表示、堆分配存储表示、块链存储表示。

定长顺序存储表示

即顺序串,使用静态分配的字符数组存储字符串中的字符序列。

字符数组的长度预先用 maxSize 指定,一旦空间存满不能扩充。

有两种实现方法:

        字符存放于字符数组的 0~maxSize 单元,另外用整数n记录串中实际存放的字符个数;

        字符存放于字符数组的 1~maxSize 单元,用 0 号单元记录串中实际存放的字符个数。

在此采用前者

按照 C 语言规定,在字符串值最后有一个特殊的“\0”表示串值的结束。因此,在存放串值时要求为它留一个位置。

定长存储表示的定义如下:

#define maxSize 256		//顺序串的预设长度
Typedef struct {			//顺序串的定义
    char ch[maxSize+1];		//存储数组
    int n;				//串中实际字符个数
} SeqString;

堆分配存储表示

即堆式串。字符数组的存储空间是动态分配的。串的最大空间数 maxSize 和串中实际字符个数 n 保存在串的定义中。

可以根据需要,随时改变字符数组的大小。

#define defaultSize 256;
typedef struct {
      char *ch;             //串的存储数组
      int maxSize;	   //串数组的最大长度
      int n;           	   //串的当前长度
} HString;

void initStr ( HString& S ) {
//初始化:创建字符串 S 的存储空间并置空串
      S.ch = ( char * ) malloc ((defaultSize+1)*sizeof(char));			  //分配字符数组空间
      if ( S.ch == NULL ) exit (1);   //判断分配成功与否
      S.ch[0] = ‘\0’;			  //置空串
      S.maxSize = defaultSize;  	  //置串的最大字符数
	  S.n = 0;				  //实际字符数置0
}
void createStr ( HString& s, char *init ) {
//从字符数组 init 构造串 s, 要求 s 已存在并初始化
    s.maxSize = defaultSize;
	s.ch = ( char *) malloc (( defaultSize+1 )*sizeof  
         ( char )); 
	s.n = strlen ( init );  strcpy ( s.ch, init ); 
}

void copyStr ( HString& s, HString& t ) {
//把串 t 复制给串 s , 要求 s 已存在并初始化
	s.maxSize = defaultSize;
	s.ch = ( char *) malloc (( defaultSize+1 )*sizeof( char ));
	s.n = t.n;  strcpy ( s.ch, t.ch ); 
}

void printStr ( HString& s ) {
//打印字符串 s
	printf ("串长度=%d, 最大长度=%d\n",s.n, maxSize);
	for ( int i = 0; i < s.n; i++ )
		if ( s.ch[i] == '\0' ) break;
		else printf ( "%c", s.ch[i] );
	printf ( "\n");
}

提取子串的算法示例

HString subStr ( HString& s, int pos, int len ) {	
//在串 s 中连续取从 pos 开始的 len 个字符,构成子串
//返回。若提取失败则函数返回NULL
	HString tmp;
	tmp.ch = ( char *) malloc (( defaultSize+1 )*sizeof( char ));			//创建子串空间
	tmp.maxSize = defaultSize;
	if ( pos < 0 || len < 0 || pos+len-1 >= s.maxSize )					//参数不合理,返回空串
		{ tmp.n = 0;  tmp.ch[0] = '\0'; }
	else {	
		if ( pos+len-1 >= s.n ) len = s.n-pos;				//若提取个数超出串尾,修改个数
		for ( int i = 0, j = pos; i < len; i++, j++ ) 
                tmp.ch[i] = s.ch[j];	//复制子串的字符
	   	tmp.n = len;  tmp.ch[len] = '\0';
	}
	return tmp;			//返回复制的子串
}

例:串 st = “university”,  pos = 3,  len = 4

    使用示例   HString t = subStr(st, 3, 4)

    提取子串   t = “vers”

void concatStr ( HeapString& s, HeapString& t ) {
//函数将串 t 复制到串 s 之后,通过串 s 返回结果,
//串 t 不变。
    if ( s.n+t.n <= s.maxSize ) { 							//原空间可容纳连接后的串
		for ( int i = 0; i < t.n; i++ ) 
                s.ch[s.n+i] = t.ch[i];	//串 t 复制到串 s 后
		s.n = s.n+t.n;  s.ch[s.n] = '\0';
	}
	else {		//原空间容不下连接后的串
		char *tmp = s.ch;  s.maxSize = s.n+t.n;	
		s.ch = ( char* ) malloc (( s.maxSize+1 )*sizeof 
               ( char ));	   //按新的大小分配存储空间
          strcpy ( s.ch, tmp ); 	//复制原串 s 数组
          strcat ( s.ch, t.ch );		//连接串 t 数组
          s.n = s.n+t.n;  free ( tmp );
	}
}

块链存储表示

使用单链表作为字符串的存储表示,此即字符串的链接存储表示。

链表的每个结点可以存储 1 个字符,称其“块的大小”为 1,也可以存储 n 个字符,称其“块的大小”为 n。

定义存储密度为:

显然,存储密度越高,存储利用率越高。

 结点大小为 4 时,存储利用率高,但操作复杂,需要析出单个字符;结点大小为 1 时,存储利用率低,但操作简单,可直接存取字符。

块链存储表示一般带头结点,设置头、尾指针。

字符串的模式匹配

kmp算法,这个算法课学过了,在此不再赘述,核心就是求next数组,(我叫last

复杂度o(n)

void get_last(string t,int len){
    int i=0,j=last[0]=-1;
    while(i<len){
        while(j!=-1 && t[i]!=t[j]) j=last[j];
        last[++i]=++j;
        num[i]=num[j]+1;
    }
}

多维数组和广义表

数组

数组是一种很特殊的数据结构:

        数组是存储结构,是语言内建的数据类型。

        它可以成为多种数据结构的存储表示。它的操作只有按下标“读/写”。

        数组又是逻辑结构,用于问题的解决。

        可以有查找、定位、插入、删除等操作。

        一维数组的数组元素为不可再分割的单元素时,是线性结构;

        但它的数组元素是数组时,是多维数组,是非线性结构。

一维数组

定义  数组是相同类型的数据元素的集合,而一维数组的每个数组元素是一个序对,由下标(index)和值(value)组成。

 在高级语言中的一维数组只能按元素的下标直接存取数组元素的值。

 一维数组的连续存储表示

设每个数组元素占据相等的 l 个存储单元,第 0 号元素的存储地址为 a,则第 i 号数组元素的存储地址 LOC(i) 为:

 一维数组的定义和初始化

void main ( ) {
    int a[3] = { 3, 5, 7 }, *elem, i; 	      //静态数组
    for ( i = 0; i < 3; i++ )  printf ( “%d”, a[i] );
    printf ( “\n” );               
    elem = (int *) malloc (3*sizeof (int));  //动态数组
    for ( i = 0; i < 3; i++ ) scanf (“%d”, &elem[i] );
    for ( i = 0; i < 3; i++ )
        { printf ( “%d”, *elem );  elem++; } 
    printf ( “\n” );
}	

多维数组

多维数组属于数组套数组,可以看做是一维数组的推广。多维数组的特点是每一个数据元素可以有多个直接前驱和多个直接后继。

例如,二维数组可以视为其每一个数组元素为一维数组的一维数组,但从整体来看,每一个数组元素同时处于两个向量(行、列),它可能有两个直接前驱,有两个直接后继。

必须有两个下标(行、列)以标识该元素的位置。

数组元素的下标一般具有固定的下界和上界,因此它比其他复杂的非线性结构简单。

 二维数组的连续存储表示

一维数组常被称为向量(Vector)。 二维数组 A[m][n] 可看成是由 m 个行向量组成的向量,也可看成是由 n 个列向量组成的向量。 一个二维数组类型可以定义为其分量类型为一维数组类型的一维数组类型:

  typedef T array2[m][n];       //T为元素类型

等价于:

         typedef T array1[n];              //行向量类型
         typedef array1 array2[m];    //二维数组类型

同理,一个三维数组类型可以定义为其数据元素为二维数组类型的一维数组类型。

静态定义的数组,其维数和维界在数组定义时指定,在编译时静态分配存储空间。一旦数组空间用完则不能扩充。

动态定义的数组,其维界不在说明语句中显式定义,而是在程序运行中创建数组时通过动态分配语句 malloc 分配存储空间和初始化,在撤销数组时通过 free 语句动态释放。

用一维内存来表示多维数组,就必须按某种次序将数组元素排列到一个序列中。

二维数组的动态定义和初始化

静态定义二维数组

	#define m 30
    int A[m][m];

动态存储分配建立的二维数组

	int **A;  int m =  10, n = 6, i, j;          
    *A = (int **) malloc (m*sizeof (int *));
    for ( i = 0; i < m; i++ ) 
	    A[i] = (int *) malloc (n*(int));
    for ( i = 0; i < m; i++ )
        for ( j = 0; j < n; j++ )  scanf (“%d”, &A[i][j] ); 

动态回收也需要分两步:

 for ( i = 0; i < m; i++) free (A[i]);  free (A);

二维数组中数组元素的顺序存储

 用一维数组描述它的连续存放方式

行优先存放(以行为主序):     

设数组开始存放位置为 a,  每个元素占用 l 个存储单元,则a[i][j]的存储位置为:         

Loc (a[i][j]) = a+(i*m+j )*l     

其中,m 是每行元素个数,即列数。

列优先存放(以列为主序):    

设数组开始存放位置为a,  每个元素占用 l 个存储单元,则a[i][j]的存储位置为:             

Loc(a[i][j]) = a+(j*n+i)*l     

其中,n 是每列元素个数,即行数。

三维数组

各维元素个数为  m1, m2, m3

下标为 i1, i2, i3的数组元素的存储地址:

        (按页/行/列存放)

 一般情况:n 维数组

各维元素个数为  m1, m2, m3, …, mn

下标为 i1, i2, i3, …, in 的数组元素的存储地址:

特殊矩阵的压缩存储

特殊矩阵是指非零元素或零元素的分布有一定规律的矩阵。

特殊矩阵的压缩存储主要是针对阶数很高的特殊矩阵。

为节省存储空间,对可以不存储的元素,如零元素或对称元素,不再存储。 有三种特殊矩阵:

        对称矩阵

        三对角矩阵

        w 对角矩阵

设有一个 nn 的矩阵 A。如果在在矩阵中,aij = aji,则此矩阵是对称矩阵。

若只保存对称矩阵的对角线和对角线以上 (下) 的元素,则称此为对称矩阵的压缩存储。

 若只存对角线及对角线以上的元素,称为上三角矩阵;若只存对角线或对角线以下的元素,称之为下三角矩阵。

把它们按行存放于一个一维数组 B 中,称之为对称矩阵 A 的压缩存储方式。 数组 B 共有n*(n+1)/2 个元素。

 若 i≥j, 数组元素a[i][j]在数组B中的存放位置为

 若 i<j,数组元素 A[i][j] 在矩阵的上三角部分,  在数组 B 中没有存放,可以找它的对称元素

 反过来,若已知某矩阵元素位于数组 B 的第 k 个位置,可寻找满足

 的 i, 此即为该元素的行号。

 此即为该元素的列号。

例,当 k = 8,   3*4 / 2 = 6  k < 4*5 / 2 =10,取 i = 3。则 j = 8 - 3*4 / 2 = 2。

 若 i≤j,数组元素A[i][j]在数组B中的存放位置为

 

 若 i≤j,数组元素A[i][j]在数组B中的存放位置为

 若i>j,数组元素A[i][j]在矩阵的下三角部分,在数组 B 中没有存放。因此,找它的对称元素A[j][i]。A[j][i]在数组 B 的第 (2*n-j-1) * j / 2 + i 的位置中找到。

三对角矩阵的压缩存储

 三对角矩阵中除主对角线及在主对角线上 下最临近的两条对角线上的元素外,所有其它元素均为0。总共有3n-2个非零元素。

 将三对角矩阵A中三条对角线上的元素按行存放在一维数组 B 中,且a00存放于B[0]。

 在三条对角线上的元素aij 满足

在一维数组 B 中 A[i][j] 在第 i 行,它前面有 3*i-1 个非零元素, 在本行中第 j 列前面有 j-i+1 个,所以元素 A[i][j] 在 B 中位置为 k =  2*i + j。 

 若已知三对角矩阵中某元素 A[i][j] 在数组 B[ ] 存放于第 k 个位置,则有

 例如,当 k = 8 时,

     当 k = 10 时,

w 对角矩阵的压缩存储

一个 w 对角矩阵是指主对角线两侧各有(w-1)/2条次对角线,其他位置都是零元素的矩阵,所以又称为带状矩阵(w为奇数)。

 非零元素 ai, j 的下标应满足

 如果把它的 w 条对角线元素按行优先方式存放到一个一维数组B中,为找到元素ai,j 在 B 中位置,一种简化处理是先把 w 条对角线上的元素压缩在一个 nw 的二维数组 A' 中, 让a'0,0 存放在B0,就可以简单地找到 ai,j 的存储位置了。

对于一个 w = 5 的 w 对角矩阵,对应的 A' 矩阵如下图所示。从 ai.j 到a't,s的映射关系为:

        t = i,  s = j-i+(w-1)/2 

 如i = 0, j = 0, 则t = 0, s = 2;i = 3, j=4, 则t=3, s=3。 

 矩阵元素 a't,s 在 B 中对应的存放位置 k 为 t*w+s,可得 w 对角矩阵元素 ai,j在数组 B 中位置为         k = i*w+j-i+(w-1)/2      (w = 5, (w-1)/2 = 2)

例如,当 i = 0, j = 0 时,k = 0*5+0-0+2 = 2. 当 i =  2, j = 4 时, k = 2*5+4-2+2 = 14. 当 i =  4, j = 2 时, k = 4*5+2-4+2 = 20.

稀疏矩阵

 设矩阵 A 中有 s 个非零元素。

令 e = s/(m*n),称 e 为矩阵的稀疏因子。

有人认为 e≤0.05 时称之为稀疏矩阵。

在存储稀疏矩阵时,为节省存储空间,应只存储非零元素。但通常非零元素的分布没有规律,故在存储非零元素时,必须记下它所在的行和列的位置 ( i, j )。

每一个三元组 (i, j, aij) 唯一确定了矩阵A的一个非零元素。因此,稀疏矩阵可由表示非零元素的一系列三元组及其行列数唯一确定。

稀疏矩阵的顺序存储表示

把所有记录稀疏矩阵非零元素的三元组按行为主序的方式存储在一个称为“三元组表”的一维数组(向量)中,即为稀疏矩阵的顺序存储表示。

在三元组表中,行为主序,所有非零元素的三元组按行号递增的顺序排列;行号相等的按列号递增的顺序排序。

三元组表中三元组的个数记忆在变量Terms 中,此即矩阵中的非零元素个数。稀疏矩阵的行数和列数分别记忆在 Rows 和 Cols中。

 稀疏矩阵的定义

# define maxTerms 30	//三元组表默认大小
typedef int DataType;	//矩阵元素数据类型
typedef struct {		//三元组定义
     int row, col;		//非零元素行号/列号
     DataType value;	//非零元素的值
} Triple;			

typedef struct {		//稀疏矩阵结构定义
    int Rows, Cols, Terms;	//矩阵行、列、非零元素
    Triple elem[maxTerms];    //三元组表
} SparseMatrix;

 带行指针数组的二元组表

稀疏矩阵的三元组表可以用带行指针数组的二元组表代替。

在行指针数组中元素个数与矩阵行数相等。第 i 个元素的下标 i 代表矩阵的第 i 行,元素的内容即为稀疏矩阵第 i 行的第一个非零元素在二元组表中的存放位置。

二元组表中每个二元组只记录非零元素的列号和元素值,且各二元组按行号递增的顺序排列。

与三元组表相比,省去了重复的行号。

稀疏矩阵的链接表示

在执行稀疏矩阵 (+、-、*、/)操作时,稀疏矩阵的非零元素会发生动态变化,这时,使用三元组表有双重缺陷:

         (1) 不能直接访问矩阵元素;

        (2) 插入或删除时可能发生大量元素移动;

用稀疏矩阵的链接表示可以避免这些情况。

稀疏矩阵的链接表示采用十字链表:行链表与列链表十字交叉。行链表与列链表都是带头结点的单链表。用头结点表征是第几行,第几列。

稀疏矩阵的头结点

head = true 是头结点的标识。

第 i 行与第 i 列共用一个头结点,用 next 链接。链表中按行列号顺序链接各行列的头结点。

right 指向该行链表首元结点的指针;down 指向该列链表首元结点的指针。

对链表扫描从头结点开始,最后以NULL结束。

稀疏矩阵的元素结点的结构

head = false是稀疏矩阵元素结点的标识。

row 和 col 是非零元素的行/列号,value 是该非零元素的值。

right 是在行链表中指向该行下一个非零元素结点的指针;down是在列链表中指向该列下一个非零元素结点的指针。

每一元素结点同时处于某行某列链表中。

 稀疏矩阵的转置

一个 mn 的矩阵 A,它的转置矩阵 B 是一个 nm 的矩阵,且 A[i][j] = B[j][i]。即

矩阵 A 的行成为矩阵 B 的列

矩阵 A 的列成为矩阵 B 的行。

在稀疏矩阵的三元组表中,非零矩阵元素按行存放。

当行号相同时,按列号递增的顺序存放。

如果稀疏矩阵的转置运算基于三元组表,则矩阵的转置要直接对相应三元组表进行转置。

 

 

 稀疏矩阵转置算法思想

设矩阵列数为 Cols,对矩阵三元组表扫描Cols 次。

第 k 次检测列号为 k 的项。

第 k 次扫描找寻所有列号为 k 的项,将其行号变列号、列号变行号,连同该元素的值,顺次存于转置矩阵三元组表。

若设矩阵非零元素有 Terms 个,则上述二重循环执行的时间复杂性为O(Cols×Terms)。

若矩阵有 200 行,200 列,10,000 个非零元素,总共有 2,000,000 次处理。

#include "SparseMatrix.cpp"
void Transpose ( SparseMatrix& a, SparseMatrix& b ) {
//稀疏矩阵 a 转置,在 b 中得到结果
    int CurrentB, k, i;
    b.Rows = a.Cols;  b.Cols = a.Rows;
    b.Terms = a.Terms;
   	if ( a.Terms > 0 ) {
        CurrentB = 0;		    //转置三元组表存放指针
        for ( k = 0; k < a.Cols; k++ )            //按列号处理
            for ( i = 0; i < a.Terms; i++ ) 	
			      //在三元组表中找列号为 k 的三元组
        if ( a.elem[i].col == k ) {	    //第 i 项列号为 k
            b.elem[CurrentB].row = k;	
            b.elem[CurrentB].col = a.elem[i].row;
 		 b.elem[CurrentB].value = a.elem[i].value;
 		 CurrentB++;	              //存放指针进1
        }
    }
}

此算法慢就慢在二重嵌套循环。若能一趟扫描过去就实现转置,运算速度将大大提高。为此,需要事先做点功课。这就是快速转置的想法。

快速转置算法 

快速转置的想法是:

对 原矩阵 a 扫描一遍,按 a 中每一元素的列号,立即确定在转置矩阵 b 三元组表中的位置,并装入它。

为加速转置速度,建立两个辅助数组 rowSize 和 rowStart:

        rowSize 记录矩阵转置前各列非零元素个数,转置后就是各行非零元素个数;

        rowStart 记录转置后各行非零元素在转置三元组表中开始存放位置。

#include "SparseMatrix.cpp"
void FastTranspos ( SparseMatrix& a, SparseMatrix& b ) 
{
//对稀疏矩阵 a 做快速转置, 结果放在 b 中
    int *rowSize = (int *) malloc (a.Cols*sizeof (int));
    int *rowStart = (int *) malloc (a.Cols*sizeof (int));
    int i, j;
    b.Rows = a.Cols;  b.Cols = a.Rows;  
    b.Terms = a.Terms; 
	if ( a.Terms > 0 ) {  
        for ( i = 0; i < a.Cols; i++ ) rowSize[i] = 0; 
				        //统计矩阵中各列非零元素数
        for ( i = 0; i < a.Terms; i++ )   
            rowSize[a.elem[i].col]++;
        rowStart[0] = 0;      //计算转置后各行开始位置
        for ( i = 1; i < a.Cols; i++ )
            rowStart[i] = rowStart[i-1]+ rowSize[i-1];
        for ( i = 0; i < a.Terms; i++ ) {   	//从 a 向 b 传送
            j = rowStart[a.elem[i].col];	//转置存放位置
            b.elem[j].row = a.elem[i].col;
	        b.elem[j].col = a.elem[i].row;
            b.elem[j].value = a.elem[i].value;
            rowStart[a.elem[i].col]++;
			//修改第 j 行元素下一存放位置
        }
    }
    free ( rowSize );  free ( rowStart );
}

该算法有 4 个并列单重循环,各自的时间复杂度为O(Cols), O(Terms), O(Cols), O(Terms)。总的时间复杂度为O(max(Cols, Terms))。 若矩阵有 200 行,200 列,10,000 个非零元素,总共有 10,000 次处理。

广义表

广义表是 n ( ≥0 ) 个表元素组成的有限序列,记作

             LS (a1, a2, a3, …, an)

LS 是表名,ai 是表元素,可以是表(称为子表),可以是单元素(称为原子,不可再分)。

n为表的长度。n = 0 的广义表为空表。

n > 0时,表的第一个表元素称为广义表 的表头(head),除此之外,其它表元素组成的表称为广义表的表尾(tail)。

广义表的特性

有次序性

有长度

有深度

可共享

可递归

 广义表的表头与表尾

 广义表的第一个表元素即为该表的表头,除表头元素外其他表元素组成的表即为该表的表尾。

 广义表的存储表示

广义表的头尾表示

除空表外,广义表可以分为表头、表尾两部分。

表头可以是单元素,也可以是子表,但表尾一定是子表。因此,广义表的头尾表示有两种结点: 表结点。包括指向表头结点的指针hlink和指向表尾结点的指针tlink;

单元素结点。保存单元素数据值的data域。  每个结点有一个标志域tag。tag = 0,表示该结点是单元素结点;tag = 1,表示该结点是表结点。

 广义表的头尾表示的结点类型

 空表没有结点,指向空表的头指针为空。

非空表的头指针指向一个表结点。该结点的hlink指针指向表头元素结点,tlink指向表尾(该表尾肯定还是表)。  

如果有多个指针指向一个表结点,则出现共享情况;如果表中某元素是子表,其 hlink 又指向该表,则出现递归情况。

 广义表的扩展线性链表表示

广义表的扩展线性链表表示也有两种结点:原子结点和表结点。其结点类型如下:

 单元素结点的标志 tag = 0,value 存放元素的值,tlink 存放指向同一层下一表元素结点的指针;

表结点的标志 tag = 1,hlink 存放指向子表的指针hlink,tlink 与单元素结点tlink的含义相同

 与头尾表示相比,优点是每个广义表都有一个起到“头结点”作用的表结点,即使空表,也有一个表结点。

这种表示的缺点是每个对子表引用的指针没有指向子表的“头结点”,而是直接指向了广义表的表头元素结点(是第一个表元素结点),这样,造成对表头元素结点插入或删除时的困难。

如果某表头元素为多个表结点共享,删除它后如何找到所有共享它的结点,以修改指向这个结点的所有指针,这种表示显然无法胜任。  

广义表的层次表示 

在这种表示中有 3 种结点:原子结点、子表结点和头结点(非表头元素结点)。

头结点的作用是要简化插入和删除操作。如果插入或删除表头元素结点,有了头结点,就像带头结点的单链表,不必修改其他表中指向该表头的指针。

在表的头结点中存储该表的引用计数。如果要删除该链表,需要先查看引用计数,如果有多个链共享该链表,就不能删除它,只需将其引用计数减一。

广义表的层次表示中结点的类型定义

 结点类型 tag:

        = 0,  表头结点;

        = 1, 原子结点;

        = 2, 子表结点

信息info:tag = 0 时, 存放引用计数(ref);tag = 1 时, 存放数据值(value);tag = 2 时, 存放指向子表头结点的指针(hlink)

尾指针tlink:tag = 0 时, 指向该表第一个结点;tag  0 时, 指向同一层下一个结点

 广义表的结点类型定义

typedef struct node {	//广义表结点定义
     int tag;	
      //=0为头结点,=1为原子结点,=2是子表结点
     struct node *tlink;	//指向同层下一结点的指针
     union {	      //共用体,此3个域叠压在同一空间
          	char name;	 //tag=0,存放表名,设为单字符
          	char value;	 //tag=1,存放数据,设为单字符
          	struct node *hlink;    
			 //tag=2,存放指向子表的指针
     } info;
} GenListNode, *GenList;

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值