数据结构与算法(三)列表结构以及栈与队列

列表结构:要求各元素在逻辑上具有线性次序,但对其物理地址未作任何要求即动态存储策略。

列表的构建:节点类,列表类
节点类:包括接口,接口包括成员变量和成员函数。均为公有。

成员变量:当前节点值,前驱节点,后继节点。关键是如何访问前驱节点,后继节点。由于列表结构的节点都不是
物理连续存储的。所以只能用指针访问。而向量不同的是只要确定了起始地址(起始指针),和大小就可以确定所有值。总而言之不管是列表还是向量均需要指针来存放具体的元素。
也就是需要两个指针指向节点对象。

成员函数:构造函数,析构函数(释放堆上空间),插入前节点,插入后节点。

template <template T>struct ListNode{
T data,ListNode<T>* pred;ListNode<T>* succ;
ListNode() {};
ListNode(T,e,ListNode<T>* P=NULL,ListNode* s=NULL):data(e),pred(p),succ(s){}

ListNode<T>* insertAsPred(T const& e);
ListNode<T>* insertAsSucc(T const& e);
}

列表模板类:
保护成员变量:规模,头哨兵,尾哨兵。
保护成员函数:初始化,复制列表中某一段,有序列表归并,排序,清除所有节点。
公有函数:构造函数,析构函数,只读访问接口,可写访问接口,遍历。
可知向量和列表的类定义很相似,进行归纳可推广到所有的抽象数据结构。
关于遍历可以传入一个函数来作为操作器。
函数传参:
double (*pf[2])(double,double) = { add, add2 }; 
x = 2; y = 1;
calculate(x, y, pf);
add1,add2均为函数名称。

2:关于数组指针与指针数组的区别。
数组指针(也称行指针)
定义 int (*p)[n];
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
如要将二维数组赋给一指针,应这样赋值:
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
 p=a;        //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
 p++;       //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

指针数组
定义 int *p[n];
[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:
int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i]
这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]
所以要分别赋值。

这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。
还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
比如要表示数组中i行j列一个元素:
*(p[i]+j)、*(*(p+i)+j)、(*(p+i))[j]、p[i][j]

优先级:()>[]>*

数值,前驱和后继。List的私有头结点和尾节点始终存在,却对外不可见。

插入排序:前缀有序,后缀无序。将后缀元素插入到前缀中的合适位置。借助于有序序列的查找算法。稳定算法。O(n*n)

选择排序:前缀无序,后缀有序。将前缀元素插入到后缀中的合适位置。借助于无序查找最大值算法。无序查找最大值单次复杂度可降低到O(logn)。

归并排序:有序列表的二路归并排序。

基于栈结构的虚拟机。栈与队列的具体应用:每次函数调用时,会创建一帧,该帧记录返回地址,局部变量以及传入参数等,将该帧压入调用栈。若在该函数返回之前发生新的调用,则同样将新函数对应的帧压入栈顶。函数一旦运行完毕,对应的帧随即弹出,运行控制权交还给该函数上层的调用函数,并按照该帧中记录的返回地址确定在二进制程序中继续执行的位置。

对应递归:同一函数可能同时拥有多个实例,并在调用栈中各自占有一帧。这些帧的结构完全相同。在追求更高的效率场合,应该避免递归。

栈的构建:基于向量基类而派生出来的结构。

栈继承自向量:
1.包含向量的头文件
2.栈模板类公有继承:
template <typename T>class stack:public vector{
public:
    void push(T const& e){ insert(size(),e);}
    T pop() {return remove(size()-1);}
    T& top(){return (*this)[size()-1];}
}

栈的应用:

0.栈的压入顺序与弹出顺序的关系

bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        stack<int>s;
        int i=0;
        for(auto x:pushV){
            if(x!=popV[i]){
                s.push(x);
            }else{
                i++;
            }
        }
        for(int j=i;j<popV.size();j++){
            if(s.top()==popV[j]){
                s.pop();
            }else{
                return false;
            }
        }
        return true;
    }

1.逆序输出。实例为进制转换。含义是一类问题其结果是按照顺序计算得到的,但是却要求逆序输出,如果按照向量来保存中间结果,则需要将向量逆序输出,若用栈来保存结果,则只需依次出栈即可。如下:

 

 

2.递归嵌套:栈混洗,有A,B,S三个栈,A栈中有n个元素按某一序列。现将A栈中所有元素移到B栈中,得到另一个序列。条件操作:A出入S,S出入B,这两种操作各n次,实施出栈时栈不得为空。

每一栈混洗的序列均由栈S的n次PUSH和n次POP构成。条件,且任一前缀中的PUSH不少于POP操作,即为栈混洗。

应用:括号匹配,判断某一括号序列是否在嵌套的意义下完全匹配。方法一;分治策略,分析括号。算法流程如下:

输入:字符串括号表达式S
输出:true or false
1.删除S中不含括号的最长前缀和后缀
2.判断首尾字符是否正确
2.找到合法切分点,从首部到切分点这一段的左右括号数必须相同:
    将S分成两段再分别判断,递归的解决方式。
该算法缺点:复杂度O(n*n),难以处理多种括号的表达式。

方法二:用栈混洗来解决。若括号匹配,则属于栈混洗,即括号的排序满足PUSH和POP序列。只需检验其是否满足即可。算法如下:

输入:字符串表达式S
输出;true or false
1.for 循环扫描S:
2.遇到左括号直接进栈,遇到右括号直接出栈并与栈顶匹配,若匹配,则继续,否则返回false。
3.非括号字符忽略。
用switch格式,可处理多种括号匹配问题。

3.延迟缓冲。解决问题是,计算速度慢于扫描速度,等到有足够的缓存信息时才进行计算。栈在这里充当缓存。表达式求值问题。算法流程如下:

运算符:加减乘除,括号,头/尾字符‘\0’。
输入:字符串表达式S,前提语法正确,栈顶运算符与当前运算符的优先等级比较数组。
输出:计算值,RPN数学表达式
1.字符串去空格
2.头字符入运算符栈,运算符栈非空,扫描各个S各字符while循环:
    若为运算数,则入运算数栈。添加到RPN末尾。
    若为运算符,与栈顶运算符优先级比较:
        若高于栈顶,则入栈。
        若低于栈顶,则做计算,并栈顶运算符弹出并添加到RPN末尾:
            若为一元运算符,则取出一个操作数,结果入栈。
            若为二元运算符,则取出两个操作数,结果入栈。
        若等于栈顶,则为括号或者尾部符号,弹出该运算符,扫描下一个。

3.弹出运算数的栈顶=最后的计算结果。

RPN数学表达式求值:计算效率强于常规表达式。,无需考虑运算符的优先级。
输入:RPN表达式S,假定语法正确。
输出:表达式数值
{
    栈P,存放操作数;
    while(S扫描依次){
        从S中取元素X;
        if其为操作数,则入栈P;
        else{
            从栈P中弹出所需的操作数,进行X运算,结果入栈P;
        }
    }
返回栈P的值即为结果。
}

试探回溯算法:就是暴力搜索法,求取最优解问题。多个元素构成的序列,使得某一值最低,求取序列。关键是如何快速找到解。如何保证搜索的部分不被重复搜索。八皇后问题;在N*N的棋盘上,放置八个皇后,使得彼此不攻击。水平,垂直。对角线均为其势力范围。借助栈记录查找的结果,各皇后的列号。算法如下:

输入:N*N的格子
输出:栈S,存放每一个皇后的列号。
{
    栈S,新建皇后Q(0,0),从原点出发;
    do{
        if栈S是否满足结果或者Q是否越界:
            回溯一行,试探下一列即,弹出S,Q.Y++;
        else{
            试探下一行,通过与已有皇后对比:
                找到可放置下一皇后的列;
            if存在可放置的列:
                摆上当前皇后,if达成解,则全局计数加一;
                从下一行开始,0列,试探下一皇后
            }
        }while(所有分支均穷尽)
}

关于判断皇后冲突的问题;x==q.x; y==q.y; x+y==q.x+q.y; x-y==q.x-q.y;
void placeQueen(int N){
    Stack<Queen> s;
    Queen q(0,0);
    do{
        if(s.size()>=N || q.y>=N){
            q=s.pop();q.y++; 回溯的条件:改行的q越界了。不存在。或者是找到了一个解。
        }else{
            while(s.find(q) && q.y<N){
                q.y++    试探,若有冲突则为真,无冲突为假,但不能越界。即找到一个合适解
            }
            if(q.y<N){
                s.push(q);
                if(s.size()>=N) nSolu++;若没有越界,则入栈,并更新起始点。nSolu为解的个数。
                q.x++;q.y=0;
            }
        }
    }while(q.x>0 || q.y <N);终止条件为所有情况均已经尝试过了,也就是x=0,y=N为起始点时。
}

迷宫寻路:结果从起点到目标点之间找到一条通路。路径规划。用试探回溯法解决:

输入:N*N个单元格,起点坐标B和终点坐标T
输出:一条通路栈S。
条件:单元格为P,P有四种状态,可用,在栈中,墙不可用,所有方向均尝试失败后回溯过的。
P有四个方向,处于可用状态的相邻方向未知,处于失败回溯的相邻方向为noway。当然还有一个初始的状态表记录所有单元的状态。

1.if B或T的状态不可用,则返回false;
2.初始化起点B的进入方向=UNKNOWN,状态为route,入栈S。
3.do{
    取出栈顶元素,若已经到达终点则返回真。
    while(检查每一个方向){
        可用,则break;
    }
    if(所有方向均尝试过){
        则向后后退一步,出栈;
    }else{
        向前试探一步,入栈,并更新状态。
    }
}while(栈不为空)

return false;

现总结关于八皇后问题和该路径规划问题的不同点和相同点是什么?
不同点:八皇后是求出符合某一个条件的皇后位置,没有起点和终点的规定。路径规划是已知起点和终点,求出符合某一条件的路径。
相同点:均是由试探回溯策略来解决,do,while语句,栈来保存位置或路径。取初始点,判断是否成功,搜索下一个位置,若不存在则回溯,否则入栈。while判断可执行条件。
可总结试探回溯算法如下:
1.建立元素类。
2, do{
        if(达到回溯条件)
            回溯;
        else
            while(找到可用的元素)
            入栈,
}while(全部查找结束条件)

队列:先入先出原则。用列表的派生类,利用C++的继承机制实现。队列的应用:资源循环分配器,参与资源分配的客户为队列,出队,接受服务,重新入队。银行模拟业务:多个窗口,多个队列,只要有新来的顾客,只需比较其队列的size(),即可将该顾客进行入列。

列表结构操作接口:list 双向列表 因为 list 容器的 begin() 和 end() 函数返回的都是双向迭代器,所以不能用它们加减整数。修改双向迭代器的唯一方式是使用自增或自减运算符。

构造函数:
和向量一样。


接口函数:大部分和向量类似。
names.push_front("Ian");
names.push_back("Kitty");
words.unique () ;移除连续的重复元素。
names.sort(std::greater<std::string>());
my_values.merge (your_values);两个必须为升序列表。

还有一个forward_list 容器以单链表的形式存储元素。
auto count = std::distance(std::begin(my_words),std::end(my_words));

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值