线性表是最常用最简单的线性结构
线性结构具有以下基本特征:
线性结构是一个数据元素的有序(次序)集(处理元素有限)。若该集合非空,则
1)必存在唯一的一个“第一元素”;
2)必存在唯一的一个“最后元素”;
3)除第一元素之外,其余每个元素均有唯一的前驱;
4)除最后元素之外,其余每个元素均有唯一的后继。
抽象数据类型(ADT)格式:
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作
endADT
一、线性表的顺序存储结构
用一组地址连续的存储单元依次存储线性表元素,线性表的这种机内表示称作线性表的顺序存储结构或顺序映像,简称顺序表。
顺序表的特点:
a.逻辑上相邻的元素其物理位置也相邻。
b.能按逻辑序号(位序)对元素进行随机存取。
线性表顺序存储的结构代码
#define MAXSIZE 20
typedef int ElemType
typedef struct
{
ElemType data[MAXSIZE];
int length;//线性表长度
}
线性表的基本操作在顺序表中的实现
InitList(&L) // 结构初始化
ListInsert(&L,i,e) // 插入元素
ListDelete(&L,I, &e) // 删除元素
LocateElem(L,e, compare()) // 查找
#include<iostream>
using namespace std;
#define LIST_INT_SIZE 100
template<class T>
class List{
public:
List(int size);
~List();
int Locate(T x);
bool Full();
bool Empty();
bool Insert(int i,T x);
bool Delete(int i);
void input();
void output();
protected:
T *data; //存储空间基址
int listsize;
int length;
};
//构造一个空的顺序表
template<class T>
List<T>::List(int size){
listsize = size;
length = 0;
data = new T[listsize];
if(data == 0)
{
cout<<"存储分配错误!"<<endl;
exit(1);
}
}
template<class T>
List<T>::~List(){
delete[] data;
}
//定位
template<class T>
int List<T>::Locate(T x)
{
int i;
for(i=0; i<length ; i++)
{
if(data[i] == x)
return i+1;
}
if(i == length)
return -1;
}
//判定顺序表是否满了
template<class T>
bool List<T>::Full(){
return (length==listsize) ? true : false;
}
//判空
template<class T>
bool List<T>::Empty(){
return (length==0) ? true : false;
}
//插入元素
template<class T>
bool List<T>::Insert(int i,T x)
{
if(i<1 || i>length+1)
return false;
if(length >= listsize)
return false;
length++;
for(int k=length-1;k>=i-1;--k)
{
data[k]=data[k-1];
}
data[i-1]=x;
}
//删除元素
template<class T>
bool List<T>::Delete(int i)
{
if (length<=0) return false;
if(i<1 || i>length) return false;
// x = data[i-1];
for(int j=i-1; j<length-1; ++j)
data[j] = data[j+1];
length -= 1;
return true;
}
//打印顺序表
template<class T>
void List<T>::output()
{
for(int i=0;i<length;i++)
std::cout<<data[i]<<" ";
}
int main()
{
List<int> L(10);
L.Insert(1,2);
L.Insert(2,5);
L.Insert(1,10);
L.Insert(1,3);
L.Insert(1,10);
L.Insert(1,9);
L.Delete(2);
cout<<"查找元素3的位置为:"<<L.Locate(3)<<endl;
L.output();
return 0;
}
注意:链表中LinkList L与LinkList *L的区别
对于LinkList L: L是指向定义的node结构体的指针,可以用->运算符来访问结构体成员,即L->elem,而(*L)就是个Node型的结构体了,可以用点运算符访问该结构体成员,即(*L).elem;
对于LinkList *L:L是指向定义的Node结构体指针的指针,所以(*L)是指向Node结构体的指针,可以用->运算符来访问结构体成员,即(*L)->elem,当然,(**L)就是Node型结构体了,所以可以用点运算符来访问结构体成员,即(**L).elem;
在链表操作中,我们常常要用链表变量作物函数的参数,这时,用LinkList L还是LinkList *L就很值得考虑深究了,一个用不好,函数就会出现逻辑错误,其准则是:
如果函数会改变指针L的值,而你希望函数结束调用后保存L的值,那你就要用LinkList *L,这样,向函数传递的就是指针的地址,结束调用后,自然就可以去改变指针的值;
而如果函数只会修改指针所指向的内容,而不会更改指针的值,那么用LinkList L就行了;
注:c语言中->和.的区别——->用于指针, .用于对象
"->"用于指向结构成员,它的左边应为指向该结构类型的指针(结构指针),而"."的左边应为该结构类型的变量(结构变量),如已定义了一个结构体struct student,里面有一个int a;然后有一个结构体变量struct student stu及结构体变量指针struct student *p;且有p=&stu,那么p->a和stu.a表示同一个意思。
在“结构”一单元中出现的->运算符成为“右箭头选择”,在使用中可以用p->a = 10;来代替(*p).a = 10;
二、线性表的链式存储结构
有头有尾:
头指针的数据域不存储任何信息
头指针:是指链表指向的第一个结点的指针,若链表有头结点,则是指向头结点的指针;头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字);无论链表为空,头指针均不为空;头指针是链表的必要元素。
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但可以用来存放链表的长度);有了头结点,在对第一个元素结点前插入结点和删除第一结点起操作与其他结点的操作就统一了;头结点不一定是链表的必须要素。
单链表图例
空链表图例
结构指针描述单链表
typedef struct Node
{
ElemType data;
struct Node* Next;
}Node;
typedef struct Node* LinkList;
单链表的读取GetElem:
•获得链表第i个数据的算法思路:
–声明一个结点p指向链表第一个结点,初始化j从1开始;
–当j<i时,就遍历链表,让p的指针向后移动,不断指向一下结点,j+1;
–若到链表末尾p为空,则说明第i个元素不存在;
–否则查找成功,返回结点p的数据。
单链表的插入ListInsert:
•单链表第i个数据插入结点的算法思路:
–声明一结点p指向链表头结点,初始化j从1开始;
–当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
–若到链表末尾p为空,则说明第i个元素不存在;
–否则查找成功,在系统中生成一个空结点s;
–将数据元素e赋值给s->data;
–单链表的插入刚才两个标准语句;
–返回成功。
单链表的删除ListDelete:
•单链表第i个数据删除结点的算法思路:
–声明结点p指向链表第一个结点,初始化j=1;
–当j<1时,就遍历链表,让P的指针向后移动,不断指向下一个结点,j累加1;
–若到链表末尾p为空,则说明第i个元素不存在;
–否则查找成功,将欲删除结点p->next赋值给q;
–单链表的删除标准语句p->next = q->next;
–将q结点中的数据赋值给e,作为返回;
–释放q结点。
#include <stdio.h>
#include <stdlib.h>
#include<time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
typedef int ElemType;
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*LinkList;
Status InitList(LinkList *L)
{
*L = (LinkList)malloc(sizeof(Node));
if(!(*L))
{
return ERROR;
}
(*L)->next = NULL;
return OK;
}
//读取
//L为带头节点的单链表的头指针
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next;
j = 1;
while(p && j<i )
{
p = p->next;
j++;
}
if(!p || j>i)
{
return ERROR;
}
*e = p->data;
return OK;
}
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
//此时链表中带头节点
j = 1;
while( p && j<i ) // 用于寻找第i个结点
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while( p->next && j<i )
{
p = p->next;
++j;
}
if( !(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); // 初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for( i=0; i < n; i++ )
{
p = (LinkList)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1;
p->next = (*L)->next;
(*L)->next = p;
}
}
//尾插法
void CreateListTil(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
//初始化随机数种子
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for(i = 0; i<n ; i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;
}
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
void ShowList(LinkList L)
{
LinkList p;
p = L->next;
while(p)
{
printf("%d ",p->data);
p = p->next;
}
}
int main()
{
LinkList L;
Status i;
i = InitList(&L);
CreateListHead(&L,10);
ShowList(L);
return 0;
}
时间性能:
——查找:顺序存储结构O(1)、单链表O(n)
——插入和删除:顺序存储结构需要平均移动表长一半的元素,时间为O(n);单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
空间性能:
——顺序存储结构需要预分配存储空间
——单链表不需要分配存储空间,只要有就可以分配,元素个数不受限制。
综上:若线性表需要频繁查找,很少进行插入和删除操作时,可采用顺序存储结构。需要频繁进行插入和删除操作时,宜采用链式存储结构。
思考:判断单链表中是否有环——•有环的定义是,链表的尾节点指向了链表中的某个节点。
•方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。如图,当p从6走到3时,用了6步,此时若q从head出发,则只需两步就到3,因而步数不等,出现矛盾,存在环。
•方法二:(快慢指针)使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。
魔术师发牌问题:
•问题描述:魔术师利用一副牌中的13张黑牌,预先将他们排好后叠放在一起,牌面朝下。对观众说:“我不看牌,只数数就可以猜到每张牌是什么,我大声数数,你们听,不信?现场演示。”魔术师将最上面的那张牌数为1,把他翻过来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样依次进行将13张牌全部翻出,准确无误。
•问题:牌的开始顺序是如何安排的?
#include<stdio.h>
#include<stdlib.h>
#define CardNumber 13
typedef struct Node
{
int data;
struct Node *next;
}Node,*LinkList;
LinkList CreateLinkList()
{
LinkList head = NULL;
LinkList s,r;
int i;
r = head;
for(i = 1; i<= CardNumber ; i++)
{
s = (LinkList)malloc(sizeof(Node));
s->data = 0;
if(head == NULL)
{
head = s;
}
else
{
r->next = s;
}
r = s;
}
r->next = head;
return head;
}
void Magician(LinkList head)
{
LinkList p;
int j;
int Countnumber = 2;
p = head;
p->data = 1;
while(1)
{
for(j=0 ; j<Countnumber ; j++)
{
p = p->next;
if(p->data !=0 )
{
p->next;
j--;
}
}
if(p->data ==0)
{
p->data = Countnumber;
Countnumber++;
if(Countnumber == 14)
break;
}
}
}
void DestoryList(LinkList *list)
{
LinkList ptr = *list;
LinkList buff[CardNumber];
int i =0;
while(i<CardNumber)
{
buff[i++] = ptr;
ptr = ptr->next;
}
for(i=0 ; i<CardNumber; ++i)
free(buff[i]);
*list = 0;
}
int main()
{
printf("按如下顺序排列:\n");
LinkList p;
int i;
p = CreateLinkList();
Magician(p);
for(i=0 ; i<CardNumber ;i++)
{
printf("黑桃%d ",p->data);
p = p->next;
}
DestoryList(&p);
return 0;
}
三、静态链表:用数组代替指针描述单链表(游标实现法)
下标为0或者是最后一个时,不存放数据。约定下标为最后一个的元素的游标指向第一个有数据的下标,即1
约定下标为0的元素的游标存放没有存放数据的元素的下标,即5
最后一个存放数据的元素游标为0
#define MAXSIZE 1000
typedef struct
{
ElemType data; //数据
int cur; //游标
}Component, StaticLinkList[MAXSIZE];
静态链表初始化相当于初始化数组
Satus InitList(StaticLinkList space)
{
int i;
for(int i=0; i < MAXSIZE-1 ; i++)
{
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0;
}
return OK;
}
四、循环链表
循环链表和单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为null,现在则是head->next是否等于head
当表尾的操作比较频繁时,采用带尾指针的循环链表显然比较方便。
尾结点:*R
首结点:*(R->next->next)
五、双向链表
结点结构
typedef struct DualNode
{
ElemType data;
struct DualNode *prior; //前驱结点
struct DualNode *next; //后继结点
} DualNode, *DuLinkList;
双向链表的插入删除操作其实并不复杂,但是顺序非常重要。
插入操作:
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);
本章节巩固练习
题目1:快速找到未知长度单链表的中间节点
解法:
——遍历单链表确定链表长度L,然后再次从头节点出发循环L/2次找到单链表的中间结点
算法复杂度O(L+L/2)
——利用慢指针,原理:设置两个指针*search、*mid都指向单链表的头节点。其中*search的移动速度
是*mid的两倍,当*search移动到末尾节点的时候,mid正好就在中间了。(标尺思想)
#include "stdio.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /* 定义LinkList */
//*LinkList 结构体指针类型
//表示用LinkList 代替 struct Node*
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
/* 初始化顺序线性表 */
Status InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */
if(!(*L)) /* 存储分配失败 */
{
return ERROR;
}
(*L)->next=NULL; /* 指针域为空 */
return OK;
}
/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一个结点 */
while(p)
{
i++;
p=p->next;
}
return i;
}
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p = p->next;
}
printf("\n");
return OK;
}
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r=*L; /* r为指向尾部的结点 */
for (i=0; i < n; i++)
{
p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next=p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
// 创建有环链表
//r->next = p;
}
Status GetMidNode(LinkList L, ElemType *e)
{
LinkList search, mid;
mid = search = L;
while (search->next != NULL)
{
//search移动的速度是 mid 的2倍
if (search->next->next != NULL)
{
search = search->next->next;
mid = mid->next;
}
else
{
search = search->next;
}
}
*e = mid->data;
return OK;
}
int main()
{
LinkList L;
Status i;
char opp;
ElemType e;
int find;
int tmp;
i=InitList(&L);
printf("初始化L后:ListLength(L)=%d\n",ListLength(L));
printf("\n1.查看链表 \n2.创建链表(尾插法) \n3.链表长度 \n4.中间结点值 \n0.退出 \n请选择你的操作:\n");
while(opp != '0')
{
scanf("%c",&opp);
switch(opp)
{
case '1':
ListTraverse(L);
printf("\n");
break;
case '2':
CreateListTail(&L,20);
printf("整体创建L的元素(尾插法):\n");
ListTraverse(L);
printf("\n");
break;
case '3':
//clearList(pHead); //清空链表
printf("ListLength(L)=%d \n",ListLength(L));
printf("\n");
break;
case '4':
//GetNthNodeFromBack(L,find,&e);
GetMidNode(L, &e);
printf("链表中间结点的值为:%d\n", e);
//ListTraverse(L);
printf("\n");
break;
case '0':
exit(0);
}
}
}
题目2:写一个完整的程序,实现随机生成20个元素的链表(尾插法或头插法任意),并用题1方法快速查找中间结点的值并显示。
题目3:用循环链表模拟约瑟夫问题,把41个人自杀的顺序编号输出。
(约瑟夫问题):
•据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。
•然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
#include<stdio.h>
#include<stdlib.h>
typedef struct Node
{
int data;
struct Node *next;
}Node,*LinkList;
LinkList Create(int n)
{
LinkList p =NULL;
LinkList head;
head = (LinkList)malloc(sizeof(Node));
p = head;
LinkList s;
int i = 1;
if( 0 != n)
{
while(i <= n)
{
s = (LinkList)malloc(sizeof(Node));
s->data = i++;
p->next = s;
p = s;
}
s->next = head->next;
//指向第一个元素
}
free(head);
return s->next;
}
int main()
{
int n = 41;
int m = 3;
int i;
LinkList L = Create(n);
LinkList temp;
m %= n; // m在这里是等于3
while (L != L->next )
{
for (i = 1; i < m-1; i++)
{
L = L->next ;
}
//将指针指向要找的数的前一个结点处
printf("%d->", L->next->data );
temp = L->next ; //删除第m个节点
L->next = temp->next ;
free(temp);
L = L->next ;
}
printf("%d\n", L->data );
return 0;
}
题4(挑战与提高):
•提高挑战难度:编号为1~N的N个人按顺时针方向围坐一圈,每人持有一个密码(正整数,可以自由输入),开始人选一个正整数作为报数上限值M,从第一个人按顺时针方向自1开始顺序报数,报道M时停止报数。报M的人出列,将他的密码作为新的M值,从他顺时针方向上的下一个人开始从1报数,如此下去,直至所有人全部出列为止。
题5(双向循环链表实践)
–要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:
–DEFGHIJKLMNOPQRSTUVWXYZABC
–同时需要支持负数,例如用户输入-3,输出结果:
–XYZABCDEFGHIJKLMNOPQRSTUVW
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
typedef char ElemType;
typedef int Status;
typedef struct DualNode
{
ElemType data;
struct DualNode *prior;
struct DualNode *next;
}DualNode,*DuLinkList;
Status InitList(DuLinkList *L)
{
DualNode *p,*q;
int i;
*L = (DuLinkList)malloc(sizeof(DualNode));
if(!(*L))
{
return ERROR;
}
(*L)->next = (*L)->prior = NULL;
p = (*L);
for(i = 0 ; i<26 ; i++)
{
q = (DualNode *)malloc(sizeof(DualNode));
if(!q)
{
return ERROR;
}
q->data = 'A'+i;
q->prior = p;
q->next = p->next;
p->next = q;
p = q;
}
p->next = (*L)->next;
(*L)->next->prior = p;
return OK;
}
void caser(DuLinkList *L, int i)
{
if(i > 0)
{
do
{
(*L) = (*L)->next;
}while(--i);
}
if(i<0)
{
i = i-1;
(*L) = (*L)->next;
do
{
(*L) = (*L)->prior;
}while(++i);
}
}
int main()
{
DuLinkList L;
int i,n;
InitList(&L);
printf("请输入一个整数:\n");
scanf("%d",&n);
printf("\n");
caser(&L,n);
for(i = 0; i<26 ; i++)
{
L= L->next;
printf("%c",L->data);
}
printf("\n");
return 0;
}