您的位置:首页 > 理论基础 > 数据结构算法

数据结构-线性表

2015-07-18 23:51 197 查看

此系列博客是大学计算机专业数据结构课程的笔记,因为考研要考,希望以后有地方可供复习。希望对像我一样的小白有用,刚开始写博客,有很多地方不懂,不过以后会慢慢熟练的。加油!!!!!fighting!!!!!

数据结构在专业课里面算是比较简单的了,就是不知道考试的难度(没考过),不过估计也是很难的,特别是图这一章。不过笨鸟先飞,多看就会了。相信勤能补拙。

1.1 概念

1、线性表是具有相同特性的数据元素的一个有限序列。

注:注意概念中的关键字,线性表中的每一项叫数据元素,它是有限的。另外值得注意的是,线性表是可以是空表,即它的长度是0时为空表。

2、数据结构中,线性表分为顺序表和链表。下面介绍两种存储形式的概念的特性以及两者之间的比较。

1.1.1 顺序表

顺序表就是把线性表中的所有元素按照其逻辑顺序,依次存储到从指定的存储位置开始的一块连续的存储空间中。

注:说白了,在C语言中,就是数组,而数组的特点就是一块连续的存储空间。所谓的逻辑顺序,按照我的理解,就是其排列的顺序。或从小到大或者从大到小或者乱序,或者按照某个特定的规则排序。(不知道有没有理解到位!!!!)

1.1.2 链表

在链表存储中,每个节点不仅包含所有元素本身的信息,还包含元素之间逻辑关系的信息,即前驱节点包含后继节点的地址信息。

1.1.3 两种存储方式的比较:

1、顺序表的特点:

1)随机访问特性。顺序表在查找方面效率较高,只要知道下标,立马就能获取到该元素。

2)顺序表要求占用连续的存储空间。这是顺序表的存储结构决定的,它用数组表示,就必须是占用连续的存储空间。

3)存储分配只能预先进行,即静态分配。就像C语言中数组的声明一样,需要在声明的时候就需要确定其长度,例如int a[30];长度为30的int型数组。

2、链表的特点

1)不支持随机访问。因为链表中每个元素的地址保存在其前驱节点中的指针域,所以要访问该节点,必须知道其前驱节点,所以访问节点每次都是从头结点开始。

2)节点的存储空间利用率较顺序表稍低一些。

3)动态分配存储空间。链表可以任意增加链表长度和缩短链表长度,这得益于它的结构特点:链式。

注:顺序表在做插入操作时需要移动多个元素,而链表则不需要移动任何元素。

1.2 两种表的实现方式及其各种操作

首先定义几个常量,代码如下(C/C++):

#define MAXSIZE 100     //线性表的最大长度
#define DataType int    //数据的类型


1.2.1 顺序表

线性表的定义只有两个数据域,一个是存储数据信息的数组,一个是保存线性表长度的length变量。代码如下:

typedef struct {
DataType data[MAXSIZE];    //数据域
int length;                //顺序表长度
}SqList;


以下是顺序表的各种操作

1.初始化操作

顺序表的初始化操作只需要将length初始化为0即可.代码如下:

void Init(SqList &L){
L.length = 0;
}


2、插入操作

插入操作可分为以下几步(顺序表的存储开始位置为1):

(1) 判断插入位置index的合法性以及length的合法性(也可以增加顺序表的最大长度).

(2)将第index个元素的后续元素后移一位.

(3)将e值插入index位置,并将length加1.

int Insert(SqList &L, int index, DataType e) {

//首先判断index的合法性
//1 <= index <= length + 1, 元素开始下标为1,可以插入顺序表尾部
//L表的长度也有限制,必须小于定义的长度 MAXSIZE - 1
if(index < 1 || index > L.length + 1 || L.length == MAXSIZE - 1) {
return 0;   //插入失败返回0
}

int i;

//参数合法,开始插入操作
//1、先将需要移动的元素移动
for(i = L.length; i >= index; --i) {
L.data[i + 1] = L.data[i];
}

//2、最后index位置空着,将e插入
L.data[index] = e;

//3、长度加1
L.length++;

return 1;   //插入成功,返回1
}


3.删除操作

删除操作可分为以下几步:

(1) 判断插入位置index的合法性.

(2)将第index个元素的后续元素前移一位.

(3)将e值插入index位置,并将length减1.

int Delete(SqList &L, int index, DataType &e) {

//index的合法性
//index的范围是1 <= index <= L.length
if(index < 1 || index > L.length) {
return 0;   //删除失败,返回0
}

//1.找到要删除的元素并赋给e
e = L.data[index];

//2.将第index元素的后面的元素前移一位
int i;
for(i = index + 1; i <= L.length; i++) {
L.data[i - 1] = L.data[i];
}

//3.顺序表的长度减1
L.length--;
}


4.定位操作

因为顺序表是从下标为1的位置开始存储,所以可以作如下判断,查找元素值为e的顺序表中的元素时,若查找失败返回0,查找成功返回下标,成功时下标>0.代码比较简单,如下:

int LocateElem(SqList L, DataType e) {

int i;

for(i = 1; i <= L.length; i++) {
if(L.data[i] == e) {
return i;   //查找成功,返回i
}
}
return 0;   //若查找失败,返回0
}


5.遍历操作

遍历操作很简单,代码如下:

void Traverse(SqList L) {
for(int i = 1; i <= L.length; i++) {
printf(" %d", L.data[i]);
}
}


6.判断顺序表是否为空

顺序表是否为空只需要根据length的值判断即可,代码如下:

int IsEmpty(SqList L){

if(L.length > 0){
return 1;
}
return 0;
}


顺序表的实现算是比较简单的.可以加入自动增长顺序表空间的功能,只需要判断是否已经满了,若满了,只需要另外开辟一个增长的空间即可.此时顺序表的结构体需要换成指针式的.具体的代码,可以见数据结构课本.或者下载完整的代码,我将会在博客最后贴出.

下面看看链表的存储结构和各种操作实现.

1.2.2 单链表

先定义两个数据类型:

typedef int ElemType;   //数据类型
typedef int Status;     //返回值类型


单链表的定义也只有两个域,即数据域和指针域,数据域保存当前节点的数据,可以多种(代码为整形变量),指针域保存下一个节点的地址.具体代码如下:

typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkedList;


以下是单链表的各种操作

1.初始化操作:

初始化操作是生成一个头结点,并将头结点的next值赋值为NULL即可,代码如下:

Status Init(LinkedList &head) {
head = (LinkedList) malloc(sizeof(LNode));

if (!head)return ERROR;

head->next = NULL;
return OK;
}


2.插入操作:

插入操作要考虑的因素有以下几点:

(1)插入的位置的合法性

(2)单链表因为其特点,插入操作比较简单,只需要修改指针即可,所以难点是在插入位置的判断上.代码如下:

Status Insert(LinkedList &L, int i, ElemType e)

LinkedList p = L;

LinkedList s;   //待插入节点

int j = 0;      //j=0时,p指向头结点

//找到第i个节点的位置,循环结束之后,p指向第i-1个元素
while (p && j < i - 1) {
p = p->next;
++j;
}

//若i小于1,或者i大于链表长度+1
//i小于1时,循环之后j > i - 1
//若i > 长度+1,那么最后p指向的是NULL,而本应该指向的是第i-1个元素
if (!p || j > i - 1)return ERROR;

//为新节点开辟内存空间
s = (LinkedList) malloc(sizeof(LNode));

//将新元素插入其中
s->data = e;
s->next = p->next;
p->next = s;

return OK;
}


3.删除操作

删除操作原理同上.需要注意的是,删除元素的i的范围不一样.因为不能删除第length+1个元素,因为不存在.

Status Delete(LinkedList &L, int i, ElemType &e) {

LinkedList q, p = L;

int j = 0;

while (p && j < i - 1) {//find the  No. i-1 LNode
p = p->next;
++j;
}

if (!p || j > i - 1)return ERROR;

//if L has element[i], p->next is now point to it.
q = p->next;
p->next = q->next;
e = q->data;

free(q);

return OK;
}


4.获取元素

获取元素的原理也同上,需要注意的是,同插入不同.获取元素的范围和删除时一样.以下代码因为p和j的初始值不一样,所以后面的判断也是不一样的,需要注意.代码如下:

Status GetElem(LinkedList &L, int i, ElemType &e) {

LinkedList p = L->next; //这里开始指向的是第一个节点

int j = 1;//所以j也要相应的指向第一个节点

while (p && j < i) {
p = p->next;
++j;
}

if (!p || j > i)return ERROR;

e = p->data;

return OK;
}


5.一个重要但是简单的操作就是遍历操作.代码如下:

Status Traverse(LinkedList &L) {

int count = 0;

LinkedList p = L->next;

while (p) {
count++;
cout << p->data << " ";
p = p->next;
}
}


6.下面介绍两个重要的操作,就是尾插法和头插法建立单链表的操作.

头插法,即在头部插入元素,就是每个元素插入之后,会在链表的第一的位置,所以头插法建立的单链表是倒序的.

头插法关键步骤:

(1)建立新的节点保存待插入元素.

(2)将此节点的指针域(next指针指向头元素,即第一个元素,即
p->next = head->next;
).

(3)将头指针指向p节点(新建立的节点),即
head->next = p;
.

头插法代码如下:

//create a length's LinkedList by head insert method
//this method causes a reversed sequence.
void CreateListHead(LinkedList &L, int length) {

//first, create a null LinkedList with a head node
L = (LinkedList) malloc(sizeof(LNode));

L->next = NULL;

LinkedList p;

//input the array that will be inserted.
cout << "Please enter " << length << " numbers." << endl;

ElemType e[length];

for (int i = 0; i < length; i++) {
cin >> e[i];
}

//create node and insert into head->next
for (int i = 0; i < length; i++) {
p = (LinkedList) malloc(sizeof(LNode));
p->data = e[i];

//insert
p->next = L->next;
L->next = p;
}
}


尾插法和头插法正好相反,是在尾部插入,顺序是正的.

尾插法的关键步骤如下:

(1)建立一个指向末尾元素的指针,r,初始指向头结点,随着元素的插入,每次都指向末尾元素.

(2)建立一个新的元素p,用于保存插入的元素.

(3)对p赋值之后接入链表末尾,即
r->next = p;


(4)更新r的位置,即
r = p;
.

尾插法的代码如下,需要注意一些细节:

//create a length's LinkedList by rear insert method
//this method causes a normal order sequence.
void CreateListRear(LinkedList &L, int length) {

//first, create a null LinkedList with a head node
L = (LinkedList) malloc(sizeof(LNode));

//declare a new LinkedList which point to L(null at first)
//r point to the end of L when inserting.
LinkedList r = L;

//input the array supposed to be inserted.
cout << "Please enter " << length << " numbers." << endl;

ElemType e[length];

for (int i = 0; i < length; i++) {
scanf("%d", &e[i]);
}

LinkedList p;

//create node and insert into head->next
for (int i = 0; i < length; i++) {
p = (LinkedList) malloc(sizeof(LNode));

p->data = e[i];

//insert
r->next = p;
r = p;
}

r->next = NULL;//the pointer of the last elem. must be NULL.
}


至此,顺序表的单链表的存储结构和基本操作已经整理完毕.下面给出两个个例题,作为操练:

(1)请逆序打印出单链表的所有元素,假设初始时,L指针指向单链表的开始节点.

解析:开始时指向第一个节点,那么需要逆序打印的话可以考虑用递归的方法.参考代码如下:

void print(LinkedList &L){
if(L != NULL){
print(L->next);
printf("%d ", L->data);
}
}


(2)有一个int型数组,里面保存的是个位数的int数据(即0~9的数字), N <= 9, 数组的长度为N,另给出一个int i, 不能使用其他变量,设计一个算法,求出数组中的最小者.

解析:意思是,只能用数组本身和i变量,求出其最小值.下面给出一个以前常用的一个求数组最小值的算法,如下:

#include <iostream>
#include <stdio.h>

using namespace std;

#define N 10

int main() {

int a
;

for(int i = 0; i < N; i++) {
scanf("%d", &a[i]);
}

int min1 = a[0];

for(int j = 1; j < N; j++) {
if(a[j] < min1) {
min1 = a[j];
}
}

cout << min1 << endl;

return 0;
}


从以上代码可以看出,需要找出最小值,必须有一个保存当前循环的最小值的变量,而题目只给了一个i,所以i必须分成两半用.

重新研究题目意思可以知道,数组保存的数据是一个个位数,也就是说是一个十位数是0的数,可以这样分开i,用i的十位数保存当前的循环,i的个位数保存当前的最小值,因为最小值为个位数,所以i可以完成保存两个变量的使命,参考代码如下,注释已经很详细了:

void FindMin(int a[], int &i){
i = a[0]; //初始时保存第一个数
while(i / 10 < N){
//循环继续的条件,初始时i = a[0],十位为0, 每次循环之后对十位加1即可.
if(i % 10 > a[i/10]){
//用i个位上的数字,即当前最小值,和a中第i/10(i的十位数,即当前比较的a数组元素的下标)个数.
//若a[..]更小,则更新i的个位数字
i = i - i % 10;
i = i + a[i/10];
}
i += 10;//即十位上数字加1,进入下一个比较.
}
i = i % 10;//获取个位上的数字,即最小值
}


今天就到这里了,完整的代码后续会更新此到这里.谢谢~~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: