2. 数据算法与结构 - 线性结构
2. 数据算法与结构 - 线性结构
1. 表(线性表), 表长, 空表, 前驱, 后继等定义和术语
表或线性表(List)是由零个或多个数据元素组成的有限序列(按某种先后次序排列的结点序列).
结点个数n称为表长, 若n=0为空表. 第一个结点称为表头结点(首结点); 最后一个结点称为表尾结点或尾结点. 一般, 首结点在最左端,尾结点在最右端.
排在一个非前驱结点x之前的结点称为x的前驱结点, 简称前驱; 排在一个非后继结点y之后的结点称为y的后继结点, 简称后继. 若元素存在多个, 则第一个元素无前驱, 而最后一个元素无后继, 其他元素都有且只有一个前驱和后继.
如果表中的结点某个域是按照从小到大的顺序排列, 则称为有序表.
2. 线性表的基本运算(9种)
- 查找 - 根据结点值查找到存储地址;
- 插入 - 在表中插入一个新结点;
- 删除 - 删除表中的某个结点;
- 存取 - 给定结点在表中的序号i, 求第i个结点的存储地址;
- 更新 - 修改结点中某个域的值;
- 合并 - 把两个表合并成一个表;如将每个班级的学习成绩单合并为整个年级的成绩单.
- 分裂 - 把一个表分裂成两个子表, 或多个子表. 如把英语成绩表按照75分分成两个.
- 复制 - 为某个表生成一个副本;
- 排序 - 重排无序表中的结点,使之成为有序表;
3. 线性表的存储方法
线性表有两种物理存储结构:
顺序存储: 以结点为单位, 按结点在表中的次序一次存储. 顺序存储的表成为顺序表. 结点在表中的逻辑次序与其在存储器中的物理次序一致. 相邻结点存储在相邻的存储单元中. (就是在内存中找个初始地址, 把一定的内存空间给占了, 然后把相同数据类型的数据元素依次放在这块内存中)
链式存储: 每个表元素对应链表的一个存储界定. 链式存储的表成为链式表结点含有值域和链域. 分别用来存储元素的值和其后继结点的地址(单向链表). 表尾结点的链域值为空(NULL), 表示没有后继结点. 每个链表都用一个指针变量如Head指向头结点, 且一个链表就是指表头指针和一串相继的结点的总称.
4. 线性表的顺序存储结构 - 顺序表(数组)(Array)
顺序表的插入和删除:
插入可以分为无条件和有条件插入: 无条件插入直接插入在表尾, 然后表长加1即可; 有条件插入则需要遍历到第i个位置, 将i后的结点向后移动一个位置, 然后再插入到i处, 表长加1.
代码块
删除指定结点: 从删除元素位置开始遍历到最后一个元素位置, 分别将它们都向前移动一个位置进行覆盖, 表长减1.
代码块
时间复杂度分析:
最好的情况: 插入和删除操作刚好要求在最后一个位置操作, 因为不需要移动任何元素, 所以此时的时间复杂度为O(1).
最坏的情况: 如果要插入和删除的位置是第一个元素, 那就意味着要移动所有的元素向后或者向前, 所以这个时间复杂度为O(n), 效率很低.
使用顺序表存储的几种情况:
线性表的顺序存储结构, 在存, 读数据时, 不管是哪个位置, 时间复杂度都是O(1). 而在插入或删除时, 时间复杂度都是O(n), 所以它比较适合元素个数比较稳定, 不经常插入和删除元素, 而更多的操作是存取数据的应用.
优点:
无须为表示表中元素之间的逻辑关系而增加额外的存储空间
可以快速地存取表中任意位置的元素
缺点:
插入和删除操作需要移动大量元素
当线性表长度变化较大时, 难以确定存储空间的容量
容易造成存储空间的"碎片"
顺序表(数组)的查找(针对有序表):
1)顺序查找,即从左往右查找或从右往左查找,都是使用for遍历,一旦if成立,则停止查找. 其时间复杂性在最坏情况下和平均情况下都是O(n),是效率最低的查找方法.
2)使用监督元的顺序查找,若从左向右查找,则监督元在表尾添加;若从右往左查找,则监督员在表头添加。如从右向左,在即for(a[0]=x,i=n;a[i]!=x;i--); 注意,循环体为空。 如果找到,则i不为0;如果找的到,则i为0,少了一次判断.
3)自动调频查找
4)二分查找
二分查找的思路很简单,但前提必须是有序表,且是顺序存储的,结点是等长的。 然后我们将首结点的index设为left,右结点的index为right,中间的mid, 每次先判断left是否大于right,大于,则找不到,终止;小于,则可以,继续。接着判断和mid的关系,若比mid小,则将right成为mid,在左边范围找,继续判断left是否大于right。。。若比mid大,则left为mid,在右边范围找,继续判断left是否大于right,如此重复即可... 二分查找又称作折半查找. 其平均复杂性为 O(logn).
5. 线性表的链式存储结构 - 链表(Linked List)
链表中的指针以及相关概念:
在链式存储结构中, 除了要存储数据元素信息外, 还要存储它的后继元素的存储地址(指针). 存储数据元素信息的域称为数据域, 把存储直接后继位置的域称为指针域. 这两部分信息组成数据元素称为存储映像, 称为结点(Node).

在c语言中可以使用malloc和free来分配结点和回收结点, 在C++中可以使用new和delete来分配结点和回收结点.
一般称p为指针, 链表中的第一个结点的存储位置叫做头指针, 最后一个结点指针为空(NULL), 在一个节点中的指针域内也是指针.
指针所指向的结点用p->表示. 即p->表示一个结点.
指针所指向的结点的数据域用p->data表示, 它表示一个数据元素.
指针所指向的结点的指针域用p->next表示, 即p->next也是一个指针, 它指向i+1个元素.
p的值即p->的地址, 即p的值是一个结点的地址. 所以p->next的值就是p->next->的地址.
单链表:
头指针
头指针是指链表指向第一个结点的指针, 若链表有头结点, 则是指向头结点的指针.
头指针具有标识作用, 所以常用头指针冠以链表的名字(指针变量的名字).
无论链表是否为空, 头指针均不为空.
头指针是链表的必要元素.
头结点
头结点是为了操作的统一和方便而设立的, 放在第一个元素的结点之前, 其数据域一般无意义(但也可以用来存放链表的长度).
有了头结点, 对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了.
头结点不一定是链表的必须要素.
单链表的读取(遍历):
必须从第一个结点开始一个一个找下去.
对于单向链表, 就是必须从头结点起, 一个结点一个结点地向后进行访问. 使用一个"滑动"的访问指针, 每访问一个结点后, 就让访问指针滑向"下一个结点". 即先使访问指针p指向第一个结点, 如果链表没有遍历完就执行下面的步骤: 访问p->, p=p->next; 最后遍历完毕后返回.
代码块
这个算法的时间复杂度取决于i的位置, 当i=1时, 则不需要遍历, 而i=n时则遍历n-1次才可以. 因此最坏情况的时间复杂度为O(n).
单链表的插入结点和删除结点:
插入结点 (把p->看作要插入的结点)
1) 在表头处插入结点: p->next = head; head = p; 注意: 其中 = 就是赋值操作, 这里即地址间的赋值.
2) 在表中间(包括表尾)插入: 首先, 假设在q->之后插入p->,那么插入的方法是:p->next = q->next; q->next = p;
代码块

删除结点 (把q->看作要删除的结点)
1)在表头处删除结点: q = head; head = head->next; free(q); 注意: 这里q的作用是暂时接手指针, 作为一个桥梁, 最后过河拆桥即可. 我们知道虽然p是一个指针, 但是p的值是所指向结点的地址, 所以地址都没有了, 自然这个结点就被删除了.
2)在非表头处删除结点: 首先, 假设删除在p->之后的那个结点q, 那么删除的方法是:q = p->next; p->next = q->next; free(q); 也可以是: p->next = p->next->next;
代码块
时间复杂度分析:
无论是单链表插入还是删除算法, 它们其实都是由两个部分组成: 第一部分就是遍历查找第i个元素, 第二部分就是实现插入和删除元素, 所以它们的时间复杂度都是O(n).
相比较于线性表的顺序存储结构, 对于插入或删除数据越频繁的操作, 单链表的效率优势就越是明显.
单链表的整表创建:
对于每个链表来说, 它所占用空间的大小和位置是不需要预先分配划定的, 可以根据系统的情况和实际的需求即时生成. 创建单链表的过程是一个动态生成链表的过程, 从"空表"的初始状态起, 依次建立各元素结点并逐个插入链表.
头插法建立单链表
头插法从一个空表开始, 然后把新加进的元素放在头结点后的第一个位置: 先让新结点的next指向头结点之后, 然后让头结点的next指向新结点.
始终让新结点插在第一的位置, 但生成的链表中结点的次序和输入的顺序相反.
代码块
尾插法建立单链表
有个索引, 不断的指向新结点, 把新结点都插入到索引后(最后).
代码块
单链表的整表删除:
在循环体内写q = p->next; free(p); p = q;
单链表结构与顺序存储结构优缺点:
1. 存储分配方式:
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素.
单链表采用链式存储结构, 用一组任意的存储单元存放线性表的元素.
2. 时间性能:
查找
顺序存储结构O(1)
单链表O(n)
插入和删除
顺序存储结构需要平均移动表长一半的元素,时间为O(n)
单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
3. 空间性能:
顺序存储结构需要预分配存储空间, 分大了, 容易造成空间浪费,分小了,容易发生溢出.
单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制.
总结:
若线性表需要频繁查找, 很少进行插入和删除操作时,宜采用顺序存储结构.
若需要频繁插入和删除时, 宜采用单链表结构.
题目: 快速找到未知长度单链表的中间节点
普通方法: 首先遍历一遍单链表以确定单链表的长度L. 然后再次从头节点出发循环L/2次找到单链表的中间节点.
算法复杂度为:O(L+L/2)=O(3L/2)。
高级方法: 利用快慢指针
利用快慢指针原理:设置两个指针*search、*mid都指向单链表的头节点. 其中* search的移动速度是*mid的2倍. 当*search指向末尾节点的时候, mid正好就在中间了.
算法复杂度为:O(L/2)
链表的种类:
1. 按产生方式分为: 动态链表和静态链表.
2. 按结点中的链域数: 分为单向链表和双向链表,双向链表有两个链域,左链域指向前驱结点,右链域指向后继结点.
3. 按是否有附加结点(往往用作监督元结点)可以分为: 单纯链表, 带附加表头结点的加头链表和带附加表尾结点的加尾链表.
4. 按链表是否循环可以分为:循环链表和非循环链表.
静态链表:
用数组代替指针来描述单链表. 这种用数组描述的链表叫做静态链表, 这种描述方法叫做游标实现法.

我们对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据.
我们通常把未使用的数组元素称为备用链表. 如上图, 从下标5开始之后的数组元素都为空, 所以为备用链表
数组的第一个元素,即下标为0的那个元素的游标(cursor)就存放备用链表的第一个结点的下标.
数组的最后一个元素,即下标为MAXSIZE-1的游标则存放第一个有数值的元素的下标,相当于单链表中的头结点作用.
每个元素的游标指向的是下个位置的下标. 如上图, 元素A的游标为2, 2是它下个元素的下标
静态列表的插入:
将所有未被使用过的及已被删除的分量用游标链成一个备用的链表(即所有空数据的合为一个备用链表). 每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点.
根据上图, A和C之间插入B
让元素A的游标指向备用链表第一个元素的下标(2->5), 将添加的元素B放在备用链表第一个位置, 再将这里元素B的游标指向原本元素A的下个元素的下标, 即元素C的下标(6->2), 最后数组的第一个元素的游标指向新备用链表第一个元素的下标(5->6).
静态列表的删除:
根据上图, 删除元素C
删除元素C, 所以其位置变为备用链表的第一元素, 其游标指向原本备用链表的第一个元素的下标(3->6), 而数组的第一个元素的游标指向删除位置的下标(6->2). 元素C的前一个元素, 元素B指向元素C的下一个元素, 元素D.(2->3)
静态链表优缺点:
优点: 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点.
缺点: 没有解决连续存储分配(数组)带来的表长难以确定的问题. 失去了顺序存储结构随机存取的特性.
总结: 静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法.
循环链表:
将单链表中终端结点的指针端由空指针改为指向头结点, 就使整个单链表形成一个环, 这种头尾相接的单链表成为单循环链表, 简称循环链表.

代码块
- 约瑟夫问题
在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏.
代码块
- 魔术师发牌问题
魔术师手中有A、2、3……J、Q、K十三张黑桃扑克牌。在表演魔术前,魔术师已经将他们按照一定的顺序叠放好(有花色的一面朝下)。魔术表演过程为:一开始,魔术师数1,然后把最上面的那张牌翻过来,是黑桃A;然后将其放到桌面上;第二次,魔术师数1、2;将第一张牌放到这些牌的最下面,将第二张牌翻转过来,正好是黑桃2;第三次,魔术师数1、2、3;将第1、2张牌依次放到这些牌的最下面,将第三张牌翻过来正好是黑桃3;……直到将所有的牌都翻出来为止。问原来牌的顺序是如何的.
代码块

- 拉丁方阵
拉丁方阵是一种n×n的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,
并且每种元素在一行和一列中恰好出现一次。著名数学家和物理学家欧拉使用拉
丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名.
拉丁方阵图例子图:
规律:
比如3 x 3的拉丁方阵,第一行是1,2,3,第二行好像都向前移了一位,然后第一个元素跑到了最后面…
用循环链表刚好.
代码块
双向链表:
每个结点都有两个指针, 一个为prior指向前一个结点, 另一个位next指向后一个结点.

双向链表的插入操作:
s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;
双向链表的删除操作:

p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
总结: 双向链表可以有效提高算法的时间性能, 但其实就是用空间来换取时间.
双向循环链表实践:

题目:
要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:
DEFGHIJKLMNOPQRSTUVWXYZABC
同时需要支持负数,例如用户输入-3,输出结果:
XYZABCDEFGHIJKLMNOPQRSTUVW
代码块
6. 栈(Stack)的基本概念
栈(Stack)是一个后进先出(Last in first out, LIFO)的线性表, 它只允许在表尾进行删除和插入操作.
栈的元素必须"后进先出". 例如浏览器的后退, 编辑时的撤销, 都是用栈的基本原理实现的.
允许插入和删除的那一端叫做栈顶(top), 另一端叫做栈底(bottom).
栈插入(Push)叫做入栈, 或压栈;栈删除(Pop)叫做退栈, 或出栈.
指针top用来指向栈顶, 每当进退栈时, 就"移动"栈顶指针top, 使其始终指向当前栈顶.
栈的本质是一个线性表, 线性表有两种存储形式, 那么栈也有分为栈的顺序存储结构和栈的链式存储结构, 即顺序栈和链式栈.
由于顺序存储更为方便, 所以一般情况下没有必要采用链式存储。
栈的顺序存储结构
最开始栈中不含有任何数据, 叫做空栈, 此时栈顶就是栈底. 然后数据从栈顶进入, 栈顶栈底分离, 整个栈的当前容量变大. 数据出栈时从栈顶弹出, 栈顶下移, 整个栈的当前容量变小.

创建一个栈
class Stack { public: Stack(int initCap=100); Stack(const Stack& rhs); ~Stack(); void push(Item x); void pop(Item& x); private: void realloc(int newCap); Item* array; int size; int cap; }
进栈 - 向栈中存放数据
在栈顶进行, 每次向栈中压入一个数据, top指针就要+1, 直到栈满为止.
// An internal func. to support resizing of array void Stack::realloc(int newCap){ if (newCap < size) return; Item *oldarray = array; //oldarray is “point to” array array = new Item[newCap]; //create new space for array //with a size of newCap for (int i=0; i<size; i++) array[i] = oldarray[i]; cap = newCap; delete [] oldarray; } void Stack::push(Item x){ if (size==cap) realloc(2*cap); array[size++]=x; }
如果栈满, 还要求进栈, 就会栈溢出. 可以用动态数组来解决这种问题.
退栈 - 在栈顶取出数据
每当从栈内弹出一个数据, 栈顶指针随之下移, 栈的当前容量就-1.
void Stack::pop(Item& x){ if (size==0) x=EmptyStack; // assume EmptyStack is a special value else { x=array[--size]; if (size <= cap/4) realloc(cap/2); } }
如果栈空, 还要求退栈, 就会退栈失败.
时间复杂度分析: 这两个函数中都没有循环语句, 也没有递归调用, 故其消耗时间都是O(1), 即进栈和退栈的工作量和栈的长度无关, 属于最优的算法.
清空栈 - 将栈中的元素全部作废, 但栈本身物理空间并不发生改变.
销毁栈 - 释放掉该栈所占据的物理内存空间.
题目: 1. 利用栈的数据结构特点, 将二进制转换为十进制数 2. 将二进制转换为八进制数
2 -> 10 2 -> 8
使用动态数组的进退栈方法
若发生栈溢出的情况, 程序无法正常运行. 因此要给栈分配足够大的空间, 但是如果不知道应该分配多少合适, 就可以使用动态数组, 先调用malloc函数为栈顶分配空间, 一旦发生溢出, 就调用realloc函数, 要求追加空间. 使用动态数组并不能保证绝对不出错, 因为调用malloc函数和realloc函数时, 也会因为"存储空间用完"而造成分配失败.
栈的链式存储结构
栈因为只是栈顶来做插入和删除操作, 所以比较好的方法就是将栈顶放在单链表的头部, 栈顶指针和单链表的头指针合二为一.
链式栈的进退栈算法
虽然一般情况下使用顺序栈式比较好的, 但是在有些情况下还是需要使用到链式栈, 这里用单向单链表结构实现链式栈,表头指针top作为栈顶指针. 开始时, 栈空, top等于NULL.
进栈函数:
// top - 栈顶指针 // s - 存储在数组s中的栈 // x - 要求进栈元素 // 返回值:SUCC - 进栈成功;FAIL - 进栈失败 p=(ptr)malloc(sizeof(snode)); // 申请结点 if (p == NULL) return FALL; // 申请失败,进栈不成功 p -> data =x; // x进栈 p->next=top; top=p; return SUCC; // 进栈成功
退栈函数:
// top - 栈顶指针 // s - 存储在数组s中的栈 // x - 接收退栈元素 // 返回值:SUCC - 进栈成功;FAIL - 进栈失败 if(top==NULL) return FALL; x=top->data; p=top; top=top->next; free(p); return SUCC;
注意: 对于栈而言,top指向指针是从上往下指的,退栈之后,删除最上面的结点,top指针向下移动。
题目: 波兰逻辑学家Jan.Lukasiewicz发明了一种不需要括号的后缀表达式, 通常把它称为逆波兰表达式(RPN). 实现对逆波兰输入的表达式进行计算并支持带小数点的数据.
正常的表达式 -> 逆波兰表达式
a+b —> a b +
a+(b-c) —> a b c – +
a+(b-c)*d —> a b c – d * +
a+d*(b-c)—> a d b c – * +
代码块
题目: 中缀表达式转换为后缀表达式
7. 队列(Queue)的基本概念
只允许在一端进行插入操作, 在另一端进行删除操作的线性表叫做队, 或队列.
与栈相反, 队列是一种先进先出(First In First Out, FIFO)的线性表.
队的元素必须"先进先出". 例如输入缓冲区接受键盘的输入就是按队列的形式输入和输出的.
允许插入的一段叫做队尾, 允许删除的一端叫做队头. 队的插入和删除分别成为进队和出队.
指针first和last分别指向队头和对尾, 每当进队时, 就移动last指针到当前队尾; 每当出队时, 就移动first指针到当前对头.
队列的顺序存储结构
进队和出对算法
比较麻烦的是既要考虑对头指针和队尾指针, 又要考虑队满和队空的情况.
开始时设置数组q[m]数组存储一个队,且有first=last=0,其中first表示头指针,last表示尾指针,两者均为0,所以说这个队列是空的。
但是为了编程方便,我们不能让first指向队的第一个结点,last指向队的最后一个结点,因为这样在一番入队和出队之后没有办法判断对空与否,所以方法有二,一:尾指针前置。即将尾指针移到后面; 二:头指针后置,即将头指针移到前面。这样的规定之后,如果说队列为空时,我们就可以通过first=last=i来判断了。
这样,开始first=last=0, 接着,如果出队的多,入队的少,那么最终first就会赶上last,导致last=first=i。 但是如果说没有出队的了,只有不停的入队,那么尾指针就会转一圈赶上头指针,同样地,last=first=i,那么可以得出结论:不论是队空还是队满,都有可能导致first=last=i; 那么我们怎么做出区分呢?
方法一:使用计时器count。 进队时,count++, 出队时,count--,所以一旦first=last=i就表示队空或者队满,如果说count=0,则表示对空,如果count=m则表示队满。
方法二:利用首尾指针first和last的相对位置,利用first=conut判断是否为空。利用last将要赶上first表示队满。
分析: 第一种方法简单,但是要引入计时器; 第二种方法不用引入计时器,但是他以last-first的值来判断是否将要满,这样就会导致我们最多存储m-1个元素。
入队函数:
if((last+1)%m==first)return FAIL; q[last]=x; last=(last+1)%m; return SUCC;
出队函数:
if(first==last)return FAIL; x=q[first]; first=(first+1)%m; return SUCC;
队列的链式存储结构
链式队的进出队算法
和上面的说法类似,为了便于操作,链式队采用单向加头链表结构,first为首指针,last为尾指针。每当进队时,将新结点插在表尾处,并将last移动向新结点; 每当出队时,将队头元素所占结点作为新的头结点,释放原来的头结点。 对空时,frist和last同时指向头结点。
进队函数:
先申请一个结点,然后如果申请不到了,就返回错误。 将要插入队列的x置于新节点的值域,将指针p赋值给last->next,这样,就把p->给接到最后面了, 然后再将last指针指向p即可。
ptr p; p=(ptr)malloc(sizeof(snode)); // 申请结点 if(p==NULL)return FAIL; p->data=x; last->next=p; last=p; return SUCC;
出队函数:
prt p; if(first==last)return FALL; p=first; first=p->next; x=first->data; free(p); return SUCC;
栈和队的应用举例
1. 程序中断
中断是操作系统实现程序“并发执行”的重要技术,当程序要求输入输出数据时,主机向外设发出了输入输出命令,由外设完成具体的输入输出操作。由于主机比外设的速度快得多,为了提高主机运行效率,主机在发出输入输出命令之后,并不会空等着外设传输完毕再工作,而是继续运行系统中的程序。 当外设完成了输入输出命令之后,就会向主机发出一个中断信号,主机接收到中断信号之后,就会执行一个程序即 “中断服务进程” ,这个中断服务程序再利用中断信号将主机原来执行的程序打断,打断的地方即为“断点”。为了在执行完中断服务程序之后且在之后的某个合适的时间能够准确地执行原来被打断的程序,在发生中断时,就要将断点“现场”(包括断点的地址、有关的寄存器值等)作为一个栈架保存在栈里,等到执行完中断服务程序,退栈(弹出元素),恢复现场,之后再继续执行原来被中断的程序。但是如果主机同时用到多个外设,那么执行一个中断服务程序时,可能又会收到另外一个中断信号,如果这个中断比上一个中断要求更为紧迫的话,那么当前正在执行的中断服务程序就会被新的中断服务程序利用新的中断信号打断,这个断点也要作为一个栈架保存在栈里,所以栈架和栈架之间服从后进先出“栈”规则。
2. 函数的嵌套调用和递归调用
函数的嵌套调用和程序中断是一样的,区别在于函数调用是主动的,调用点已知,而中断时被动的,所以函数调用称为软中断。
程序运行期间,每当遇到调用函数时,就要暂停正在执行的函数,转而执行被调函数,等到被调函数执行完了以后,再回过头来执行原来的函数。
函数的递归调用和嵌套调用处理方法一致.
3.表达式求值算法.
8. 矩阵
矩阵就是在线性代数中学习过的矩阵, 矩阵是一种静态结构, 它的元素数是固定的, 不能在矩阵中插入或删除元素, 数学上常见的矩阵运算有:矩阵转置、求和、求逆、矩阵相乘等.
9. 字符串
字符串是有穷的字符序列,字符个数称为串长,长度为0的是空串。
字符串也是有顺序存储和链式存储。顺序存储适合存储长度较小的,或者只做简单运算的串。如果串很大,而且插入、删除频繁,则需要采用链式存储。
串的链式存储包括定长结点的链式存储和变长结点的链式存储。定长结点即选定一个合适的大小m,作为结点的长度,于是每个结点可存储的有效字符数不超过m。变长结点是采用动态方式分配各行的存储结点。
链式存储通常是以行为单位的,以换行符为界限。每行对应一个存储结点。字符在结点内是顺序存储的。这种存储方式实际上是链式存储和顺序存储的混合。好处是:在大多数情况下,对串的修改是按行进行的,所以修改只局限于结点内部,不会影响其他的结点。 这就避免了因为插入和删除而影响到其他的结点。
字符串的简单模式匹配算法
模式匹配也成为串匹配,或子串查找。 用于在某文本文件中查找某些特定的子串。其中,文本成为正文串,而要查找的内容称为模式串。 通常,正文串是一个大串,而模式串只是一个小串。
那么什么是匹配呢? 即段定一个长度为n的模式串,和一个长度为m的正文串,找出n中最小的下标i使得在m中从i开始的一段长度为n的子字符串刚好和长度为n的模式串相匹配。
这里我们知道最简单的方法就是采用“逐段适配法” --- 从正文串a的第一个字符开始,“截取”长度为n的子串,与模式串p进行试匹配,将对应的字符进行逐个比较,如果对应相等,那么说明匹配成功,否则失败。然后,再从a的第二个字符串开始截取n个字符,与模式串p试匹配....直到找到或最后也找不到。如果a剩余的长度不足n,就匹配失败,不再匹配。 可知,使用这种方法,最多需要匹配m-n+1次。最少1次。
匹配算法:
var stringP = "shijieshangzuishuaiderenshizhuzhenwei";
var stringS = "zhuzhenwei";
for (var i = 0; i <= stringP.length-stringS.length; i++) {
for (var k = i, j = 0; j < stringS.length; j++,k++) {
if (stringP[k] != stringS[j]) {
break;
}
}
if (j == stringS.length) {
console.log(i);
}
}
if (j != stringS.length) {
console.log("error");
}
字符串的KMP算法



浙公网安备 33010602011771号