3.线性表的逻辑结构,是指线性表的数据元素间存在着线性关系。
主要是指:
除第一及最后一个元素外,每个结点都只有一个前趋和只有一个后继。
在顺序存储结构中,元素存储的先后位置反映出这种逻辑关系,而在链式存储结构中,是靠指针来反映这种逻辑关系的。
4.顺序存储结构用向量(一维数组)表示,给定下标,可以存取相应元素,属于随机存取的存储结构。
5.线性表的顺序存储方式及其在具体语言环境下的两种不同实现:
表空间的静态分配和动态分配。
掌握顺序表上实现插入、删除、定位等运算的算法。
6.尽管“只要知道某结点的指针就可以存取该元素”,但因链表的存取都需要从头指针开始,顺链而行,故链表不属于随机存取结构。
要理解头指针、头结点、首元结点和元素结点的差别。
头结点是在插入、删除等操作时,为了算法的统一而设立的(若无头结点,则在第一元素前插入元素或删除第一元素时,链表的头指针总在变化)。
对链表(不包括循环链表)的任何操作,均要从头结点开始,头结点的指针具有标记作用,故头指针往往被称为链表的名字,如链表head是指链表头结点的指针是head。
理解循环链表中设置尾指针而不设置头指针的好处。
链表操作中应注意不要使链意外“断开”。
因此,若在某结点前插入一个元素或删除某元素,必须知道该元素的前驱结点的指针。
7.链表是本部分学习的重点和难点。
重点掌握以下几种常用链表的特点和运算:
单链表、循环链表、双向链表、双向循环链表的生成、插入、删除、遍历以及链表的分解和归并等操作。
并能够设计出实现线性表其它运算的算法。
8.从时间复杂度和空间复杂度的角度综合比较线性表在顺序和链式两种存储结构下的特点,即其各自适用的场合。
小结:
顺序表和链表的比较
通过对它们的讨论可知它们各有优缺点,顺序存储有三个优点:
(1)方法简单,各种高级语言中都有数组,容易实现。
(2)不用为表示结点间的逻辑关系而增加额外的存储开销。
(3)顺序表具有按元素序号随机访问的特点。
但它也有两个缺点:
(1)在顺序表中做插入删除操作时,平均移动大约表中一半的元素,因此对n较大的顺序表效率低。
(2)需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
链表的优缺点恰好与顺序表相反。
在实际中怎样选取存储结构呢?
(1)基于存储的考虑
对线性表的长度或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。
(2)基于运算的考虑
在顺序表中按序号访问ai的时间性能时O
(1),而链表中按序号访问的时间性能O(n),所以如果经常做的运算是按序号访问数据元素,显然顺序表优于链表;而在顺序表中做插入、删除时平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链表中作插入、删除,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑显然后者优于前者。
(3)基于环境的考虑
顺序表容易实现,任何高级语言中都有数组类型,链表的操作是基于指针的,相对来讲前者简单些,也是用户考虑的一个因素。
总之,两种存储结构各有长短,选择那一种由实际问题中的主要因素决定。
通常“较稳定”的线性表选择顺序存储,而频繁做插入删除的即动态性较强的线性表宜选择链式存储。
练习题:
(一)选择题:
1.以下那一个术语与数据的存储结构无关?
(A)
A.队列B.哈希表
C.线索树D.双向链表
2、一个算法应该是(B)。
A.程序B.问题求解步骤的描述
C.要满足五个基本特性D.A和C.
3、数据结构中,与所使用的计算机无关的是数据的(C)
A.存储结构B.物理结构C.逻辑结构D.物理结构和存储结构
4.算法的计算量的大小称为计算的(B)。
A.效率B.复杂性 C.现实性 D.难度
5.下列说法,不正确的是(D)。
A.数据元素是数据的基本单位
B.数据项是数据中不可分割的最小可标识单位
C.数据可由若干个数据元素构成
D.数据项可由若干个数据元素构成
6.连续存储设计时,存储单元的地址(A)。
A.一定连续B.一定不连续
C.不一定连续D.部分连续,部分不连续
7.线性表(a1,a2,…,an)以链接方式存储时,访问第i位置元素的时间复杂性为(C)。
A.O(i)B.O
(1)C.O(n)D.O(i-1)
8.对于顺序存储的线性表,访问结点和增加、删除结点的时间复杂度为(C)。
A.O(n)O(n)B.O(n)O
(1)
C.O
(1)O(n)D.O
(1)O
(1)
9.设单链表中结点的结构为(data,link)。
已知指针q所指点是指针p所指结点的直接前驱,若在*q与*p之间插入结点*s,则应执行下列哪一个操作?
(B)。
A.s->link=p->link;p->link=s
B.q->link=s;s->link=p
C.p->link=s->link;s->link=p
D.p->link=s;s->link=q
10.在一个长度为n的顺序表的表尾插入一个新元素的渐进时间复杂度为(B)。
A.O(n)B.O
(1)
C.O(n2)D.O(log2n)
11.表长为n的顺序存储的线性表,当在任何位置上插入一个元素的概率相等时,插入一个元素所需移动元素的平均个数为(B)
A.nB.n/2
C.(n-1)/2D.(n+1)/2
12.循环链表的主要优点是(D)
A.不再需要头指针了。
B.已知某个结点的位置后,能很容易找到它的直接前驱结点。
C.在进行删除操作后,能保证链表不断开。
D.从表中任一结点出发都能遍历整个链表。
(二)应用题
1、按增长率由小至大排列以下7个函数。
答:
2、数据的存储结构由哪四种基本的存储方法实现,并做以简要说明?
答:
四种表示方法
(1)顺序存储方式。
数据元素顺序存放,每个存储结点只含一个元素。
存储位置反映数据元素间的逻辑关系。
存储密度大,但有些操作(如插入、删除)效率较差。
(2)链式存储方式。
每个存储结点除包含数据元素信息外还包含一组(至少一个)指针。
指针反映数据元素间的逻辑关系。
这种方式不要求存储空间连续,便于动态操作(如插入、删除等),但存储空间开销大(用于指针),另外不能折半查找等。
(3)索引存储方式。
除数据元素存储在一地址连续的内存空间外,尚需建立一个索引表,索引表中索引指示存储结点的存储位置(下标)或存储区间端点(下标),兼有静态和动态特性。
(4)散列存储方式。
通过散列函数和解决冲突的方法,将关键字散列在连续的有限的地址空间内,并将散列函数的值解释成关键字所在元素的存储地址,这种存储方式称为散列存储。
其特点是存取速度快,只能按关键字随机存取,不能顺序存取,也不能折半存取。
3.线性表有两种存储结构:
一是顺序表,二是链表。
试问:
(1)如果有n个线性表同时并存,并且在处理过程中各表的长度会动态变化,线性表的总数也会自动地改变。
在此情况下,应选用哪种存储结构?
为什么?
(2)若线性表的总数基本稳定,且很少进行插入和删除,但要求以最快的速度存取线性表中的元素,那么应采用哪种存储结构?
为什么?
答:
(1)选链式存储结构。
它可动态申请内存空间,不受表长度(即表中元素个数)的影响,插入、删除时间复杂度为O
(1)。
(2)选顺序存储结构。
顺序表可以随机存取,时间复杂度为O
(1)。
(三)算法设计题
1.设计算法,求带表头的单循环链表的表长。
解:
intlength(LinklistL)
{
intI;
listnode*p;
I=0;
P=L;
while(p->next!
=L){
p=p->next;
I++;
}
returnI;
}
2.已知单链表L,写一算法,删除其重复结点。
算法思路:
用指针p指向第一个数据结点,从它的后继结点开始到表的结束,找与其值相同的结点并删除之;p指向下一个;依此类推,p指向最后结点时算法结束。
算法如下:
解:
voidpur_LinkList(LinkListH)
{LNode*p,*q,*r;p=H->next;/*p指向第一个结点*/
if(p==NULL)return;
while(p->next)
{q=p;
while(q->next)/*从*p的后继开始找重复结点*/
{if(q->next->data==p->data)
{r=q->next;/*找到重复结点,用r指向,删除*r*/
q->next=r->next;
free(r);
}/*if*/
elseq=q->next;
}/*while(q->next)*/
p=p->next;/*p指向下一个,继续*/
}/*while(p->next)*/
}
该算法的时间性能为O(n2)。
3.已知指针la和lb分别指向两个无头结点的单链表中的首结点。
请编写函数完成从表la中删除自第i个元素开始的共len个元素并将它们插入到表lb中第j个元素之前,若lb中只有j-1个元素,则插在表尾。
函数原型如下:
intDeleteAndInsertSub(LinkList&la,LinkList&lb,inti,intj,intlen);
答:
intDeleteAndInsertSub(LinkList&la,LinkList&lb,inti,intj,intlen)
{
intk;
LinkListp,q,prev,s;
if(i<0||j<0||len<0)
return-1;
p=la;
k=1;
prev=NULL;
while(p&&k
{
prev=p;
p=p->next;
k++;
}
if(!
p)
return-1;
q=p;k=1;
while(q&&k{
q=q->next;
k++;
}
if(!
q)
return-1;
if(!
prev)
la=q->next;
else
prev->next=q->next;
if(j==1)
{
q->next=lb;
lb=q;
}
else
{
s=lb;k=1;
while(s&&k{
s=s->next;
k++;
}
if(!
s)
return-1;
q->next=s->next;
s->next=p;
return1;
}
}
4.写一算法,将一带有头结点的单链表就地逆置,即要求逆置在原链表上进行,不允许重新构造新链表。
函数原型如下:
voidLinkList_reverse(LinkList&L);
答:
voidLinkList_reverse(LinkList&L)
{
LinkListp,q,s;
p=L->next;q=p->next;s=q->next;p->next=NULL;
while(s->next)
{
q->next=p;p=q;
q=s;s=s->next;
}
q->next=p;s->next=q;L->next=s;
}
5.写一算法,将带有头结点的非空单链表中数据域值最小的那个结点移到链表的最前面。
要求:
不得额外申请新的链结点。
函数原型如下:
voiddelinsert(LinkList&L);
答:
voiddelinsert(LinkList&L)
{
p=L->next;//p是链表的工作指针
pre=L;//pre指向链表中数据域最小值结点的前驱
q=p;//q指向数据域最小值结点,初始假定是第一结点
while(p->next!
=NULL)
{
if(p->next->datadata)//找到新的最小值结点
{pre=p;q=p->next;}
p=p->next;
}
if(q!
=L->next)//若最小值是第一元素结点,则不需再操作
{
pre->next=q->next;//将最小值结点从链表上摘下
q->next=L->next;//将q结点插到链表最前面
L->next=q;
}
}
6.编写一个算法来交换单链表中指针P所指结点与其后继结点,HEAD是该链表的头指针,P指向该链表中某一结点。
答:
单链表中查找任何结点,都必须从头指针开始。
本题要求将指针p所指结点与其后继结点交换,这不仅要求知道p结点,还应知道p的前驱结点。
这样才能在p与其后继结点交换后,由原p结点的前驱来指向原p结点的后继结点。
LinkedListExchange(LinkedListHEAD,p)
∥HEAD是单链表头结点的指针,p是链表中的一个结点。
本算法将p所指结点与其后继结点交换。
{q=head->next;∥q是工作指针,指向链表中当前待处理结点。
pre=head;∥pre是前驱结点指针,指向q的前驱。
while(q!
=null&&q!
=p){pre=q;q=q->next;}∥未找到p结点,后移指针。
if(p->next==null)printf(“p无后继结点\n”);∥p是链表中最后一个结点,无后继。
Else∥处理p和后继结点交换
{q=p->next;∥暂存p的后继。
pre->next=q;∥p前驱结点的后继指向p的后继。
p->next=q->next;∥p的后继指向原p后继的后继。
q->next=p;∥原p后继的后继指针指向p。
}
}∥算法结束。
7.已知线性链表第一个链结点指针为list,请写一算法,将该链表分解为两个带有头结点的循环链表,并将两个循环链表的长度分别存放在各自头结点的数据域中。
其中,线性表中序号为偶数的元素分解到第一个循环链表中,序号为奇数的元素分解到第二个循环链表中。
答:
算法如下:
voidsplit(ListNode*List,ListNode*&list1,ListNode*&list2)
{
list1=(ListNode*)malloc(sizeof(ListNode));
list2=(ListNode*)malloc(sizeof(ListNode));
p=list;;
q=list1;
r=list2;
len1=0;
len2=0;
mark=1;
while(p!
=null)
{
if(mark=1)
{
q->next=p;
q=q->next;
len1++;
mark=2;
}
else
{
r->next=p;
r=r->next;
len2++;
mark=1;
}
}
list1->data=len1;
list2->data=len2;
q->next=list1;
r->next=list2;
}
8.设A和B是两个单链表,其表中元素递增有序。
试写一算法将A和B归并成一个按元素值递减有序的单链表C,并要求辅助空间为O
(1)。
答:
Linklistmerge(LinklistA,LinklistB)
{
LinklistC;
Listnode*p;
C=null;
while(A&&B)
if(A->data<=B->data)
{p=A->next;A->next=C;C=A;A=p;}
else
{p=B->next;B->next=C;C=B;B=p;}
if(A)
while(A){p=A->next;A->next=C;C=A;A=p;}
else
while(B){p=B->next;B->next=C;C=B;B=p;}
returnC;
}
二、栈、队列和数组
大纲要求:
(一)栈和队列的基本概念
(二)栈和队列的顺序存储结构
(三)栈和队列的链式存储结构
(四)栈和队列的应用
(五)特殊矩阵的压缩存储
知识点:
1.栈、队列的定义及其相关数据结构的概念,包括:
顺序栈、链栈、循环队列、链队列等。
栈与队列存取数据(请注意包括:
存和取两部分)的特点。
2.掌握顺序栈和链栈上的进栈和退栈的算法,并弄清栈空和栈满的条件。
注意因栈在一端操作,故通常链栈不设头结点。
3.如何将中缀表达式转换成前缀、后缀表达式,了解对两种表达式求值的方法。
4.栈与递归的关系。
用递归解决的几类问题:
问题的定义是递归的,数据结构是递归的,以及问题的解法是递归的。
掌握典型问题的算法以及将递归算法转换为非递归算法,如n!
阶乘问题,fib数列问题,hanoi问题。
了解在数值表达式的求解、括号的配对等问题中应用栈的工作原理。
5.掌握在链队列上实现入队和出队的算法。
注意对仅剩一个元素的链队列删除元素时的处理(令队尾指针指向队头)。
还需特别注意仅设尾指针的循环链队列的各种操作的实现。
6.循环队列队空及队满的条件。
队空定义为队头指针等于队尾指针,队满则可用牺牲一个单元或是设标记的方法,这里特别注意取模运算。
掌握循环队列中入队与出队算法。
7.在后续章节中多处有栈和队列的应用,如二叉树遍历的递归和非递归算法、图的深度优先遍历等都用到栈,而树的层次遍历、图的广度优先遍历等则用到队列。
这些方面的应用应重点掌握。
8.数组在机器(内存)级上采用顺序存储结构。
掌握数组(主要是二维)在以行序为主和列序为主的存储中的地址计算方法。
9.特殊矩阵(对称矩阵、对角矩阵、三角矩阵)在压缩存储是的下标变换公式。
练习题:
(一)选择题:
1.一个栈的输入序列为1234,则(D)不可能是其出栈序列。
A.1243B.2134C.1432D.4312
2.一个递归算法必须包括(B)。
A.递归部分B.终止条件和递归部分
C.迭代部分D.终止条件和迭代部分
3.一个递归的定义可以用递归过程求解,也可以用非递归过程求解,但单从运行时间来看,通常递归过程比非递归过程(B)。
A.较快B.较慢
C.相同D.以上答案都不对
4.栈和队列都是(C)
A.顺序存储的线性表B.链式存储的线性表
C.限制存储的线性表D.限制存储的非线性结构
5.二维数组N的元素是4个字符(每个字符占一个存储单元)组成的串,行下标i的范围从0到4,列下标j的范围从0到5,N按行存储时元素N[3][5]的起始地址与N按列存储时元素(B)的起始地址相同。
A.N[2][4]B.N[3][4]
C.N[3][5]D.N[4][4]
6.设有数组A[i,j],数组的每个元素长度为3字节,i的值为1到8,j的值为1到10,数组从内存首地址BA开始顺序存放,当以列为主序存放时,元素A[5,8]的存储首地址是(B)
A.BA+141B.BA+180
C.BA+222D.BA+225
7.递归过程或函数调用时,处理参数及返回地址,要用一种称为(C)的数据结构。
A.队列B.多维数组
C.栈D.线性表
8.对于单链表形式的队列,队空的条件是(A)
A.F=R=nil B.F=R
C.F≠nil且R=nil D.R-F=1
9.若循环队列以数组Q[0..m-1]作为其存储结构,变量rear表示循环队列中的队尾元素的实际位置,其移动按rear=(rear+1)Modm进行,变量length表示当前循环队列中的元素个数,则循环队列的队首元素的实际位置是(C)
A.rear-length
B.(rear-length+m)Modm
C.(1+rear+m-length)Modm
D.M-length
(二)应用题
1、(10分)假设一个准对角矩阵
按以下方式存储于一维数组B[4m]中:
0
1
2
3
4
k
4m-2
4m-1
......
......
写出由一对下标(i,j)表示的k的转换公式。
答:
i为奇数时k=i+j-2
i为偶数时k=i+j-1
合并后可写成k=i+j-(i%2)-1或k=2(i/2)+j-1
2、特殊矩阵和稀疏矩阵哪一种压缩存储后失去随机存取的功能?
为什么?
答:
特殊矩阵指值相同的元素或零元素在矩阵中的分布有一定规律,因此可以对非零元素分配单元(对值相同元素只分配一个单元),将非零元素存储在向量中,元素的下标i和j和该元素在向量中的下标有一定规律,可以用简单公式表示,仍具有随机存取功能。
而稀疏矩阵是指非零元素和矩阵容量相比很小(t<用十字链表作存储结构自然失去了随机存取的功能。
即使用三元组表的顺序存储结构,存取下标为i和j的元素时,要扫描三元组表,下标不同的元素,存取时间也不同,最好情况下存取时间为O
(1),最差情况下是O(n),因此也失去了随机存取的功能。
3、有人说,采用循环链表作为存储结构的队列就是循环队列,你认为这种说法对吗?
说明你的理由。
答:
这种说法是错误的。
队列(包括循环队列)是一个逻辑概念,而链表是一个存储概念,一个队列是否是循环队列。
不取决于它将采用何种存储结构。
根据实际的需要,循环队列可以采用顺序存储结构,也可以采用链式存储结构,包括采用循环链表作为存储结构。
4、指出下列程序段的功能是什么?
(1)voiddemo1(seqstack*s)
{
intI;arr[64];n=0;
while(!
stackempty(s))arr[n++]=pop(s);
for(I=0;}
(2)voiddemo2(seqstack*s,intm)
{
seqstackt;inti;
initstack(t);
while(!
Stackempt