[C/C++/数据结构]1.1 线性表的顺序表示和实现

7 篇文章 0 订阅
4 篇文章 0 订阅
如果您想直接调试代码不看分析的话,请跳到对应的分文件处下载头文件,函数定义文件以及调试文件
分文件下载可能跟这里写的不一致,请以分文件处代码为准
在用C语言时我会一直借用一些CPP的概念,知道是啥就行了

1 线性表的定义和特点

定义和特点

  • 是一个具有相同特性的数据元素的优先序列,(a_1,a_2, …,a_i,…,a_n)
  • 第一个元素是起点,他没有前驱;最后一个元素是终点,他没有后继;中间的元素有前驱和后继
  • n=0时,这个表是个空表
  • ADT List的定义如下:
    ADT List
    {
    	数据成员:D={a_i| i=1,2,...,n,n>=0}
    	
    	//序偶关系
    	数据关系:R={<a_(i-1),a_i>|a_i ∈D,i=2,...,n}
    	
    	函数成员(方法):
    		1 InitList(&List);//初始化一个空的线性表L(n=0)
    		
    		2 DestroyList(&L);//当L是存在的;我们进行销毁L(从内存中)
    		
    		3 ClearList(&L);//当L是存在的;我们清空L的元素,将其置为空(n=0)
    		
    		4 ListEmpty(L);//当L存在;判断L是(TURE)否(FALSE)为空表(n=0)
    		
    		5 ListLength(L)//当L存在;返回L的长度(元素个数)
    		
    		6 GetElem(L,i,&e);//当L存在,且i∈[1,ListLength(L)];返回L中第i个元素的值
    		
    		7 LocateElem(L,e,compare());
    		//当L存在;根据某种比较条件(由compare()传入)用L的元素跟e(具体值)比较,
    		//若L中存在元素满足条件,返回首个对应元素的位置,否则0			
    		
    		8 PriorElem(L,cur_e,&pre_e);//当L存在;若cur_e有前驱则返回前驱(即非首元素),否则返回0
    		
    		9 NextElem(L,cur_e,&next_e);//当L存在;若cur_e有后继则返回后继(即非最后元素),否则返回0
    
    		10 ListInsert(&L,i,e);//当L存在,且i∈[1,ListLength(L)+1];在L的第i个位置插入一个新元素e,L长度+1
    		
    		11 ListDelete(&L,i,&e);//当L存在,且i∈[1,ListLength(L)];删除L的第i个元素,并用e返回被删的数值,L长度-1
    		
    		12 ListTraverse(&L, visited());//L存在;对线性表的每个元素都施加visited(),可能是都输出一下,可能是都+1
    };
    

2 顺序存储

定义

  • 把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。

  • 这种存储方式的好处是,知道a_i所在的存储位置,我们能快速的访问到另一个数据比如说a_(i+10)的位置,然后调用a_(i+10)这个数据。


  • 对应到C/C++中,这不巧了嘛这不是,他们自带的数组array数据类型在很大程度上满足了这个顺序存储,因为array是在内存的栈空间上开辟一片连续的空间连续存储array中的数据(一个空间一个数据),只不过这个数组是固定大小的。
  • 所以我们只能一开始定义一个足够大的(够我们用就行)的数组,同时定义另一个变量去存储这个数组的长度。当我们把这两个数据(数组,长度)抽象成同一种类型“线性表”时(你也可以不用自定义类型),我们就有第一段代码:
#define MAXSIZE 100

typedef struct
{
//ElemType 是具体的数据类型,可以是int,char,可以是自定义类型
    ElemType elem[MAXSIZE];
    int length;
}SqList;

对于ElemType这里,为了解决更广泛的问题,我们可能需要自己的自定义类型,比如说可能在“同个位置”上,同时存在几种数据,比如说这个位置是小明,他有年龄,他有学历等等,那么我们抽象一下定义一个自定类型:

#define MAXSIZE 100
#define ElemType Person
typedef struct 
{
	int age;
	char name;
}Person;

typedef struct
{
    ElemType elem[MAXSIZE];
    int length;
}SqList;

注意以下这个语句,它意味着在栈中开辟一片MAXSIZE大小为Person类型的连续内存空间,而栈中数据能不能销毁不是程序员控制的,但是动态数组他是在堆中建立的,是我们能够控制的,所以我们这里会改成指针形式,方便我们在后面初始化时使用动态数组

ElemType elem[MAXSIZE];->Person *elem;

3 方法,“成员函数”的实现(C语言)

3.1 InitList(&List)

// 1 InitList(&List)
//注意:一般来说你成功运行了,exit码是0(想想你main为什么return 0),
//所以我并不依照老师的逻辑
int InitList(SqList *L)
{
    //这里就!!部分!!回答了为什么用Person *elem的声明
    //但注意了,elem是没有被初始化的等于说 int arr[n];一样,他的初始化只能用循环来弄
    (*L).elem = (ElemType *)malloc(sizeof(ElemType) * MAXSIZE);
    
    //如果分配空间失败,直接报错,并且退出
    //至于为什么可能分配失败,可能是内存中的栈区已经被撑爆了
    if (!L)
    {
     //EXIT_FAILURE是一个宏,翻译过来替换了1,
     //所以如果发现L是个空指针他直接终止InitList,并报告(perror)错误
        perror("Init Error: ");
        exit(EXIT_FAILURE);
    }

    L->length = 0;
    return 0;
}

我们可以测试一下InitList是不是按我们预想中操作

//这里是分文件操作的预处理指令,请您使用前对这块代码进行部分修改
#include "Linear.h"

int main()
{
	SqList L;
	L.elem;
	InitList(&L);
	printf("length is %d", L.length);
	return 0;
}

L.length他真的变成0了,这也意味着内存分配成功(因为exit和perror都没触发),如果你使用VS,你甚至可以追踪&L->elem这个地址是被怎样开辟出来的。关于测试使用这块,我最终只会放一个user文件合集,不再每个函数单独列出来了。

3.2 DestroyList(&L)

既然有初始化的了,那么我们立刻连着相似的逻辑销毁这个线性表吧!

int DestroyList(SqList* L)
{
//因为我们动态分配的是L中的elem,所以释放的也是L中的elem
//这里是防止User没有用Init直接用Destroy,这会对一块莫名其妙的区域释放内存
    if (!L->elem)
    {
        perror("Destroy Error: ");
        exit(EXIT_FAILURE);
    }
    //free完后一定要置空,防止他变成一个野指针
    free(L->elem);
    L->elem = NULL;
    L->length = 0;
    printf("Destroy Sucessful!\n");
    return 0;
}

3.3 ClearList, isEmpty, ListLen

int ClearList(SqList* L)
{
    L->length = 0;
    return 0;
}

//C里面没有bool类型,这里返回的int应该与bool一致,
//而不是之前那种0是程序正常运行,1是异常
int isEmpty(SqList* L)
{
    //==0,就是真的返回1
    return L->length == 0 ? 1 : 0;
}

int ListLen(SqList* L)
{
    return L->length;
}

3.4 GetElem, ListInsert, ListDelete

ElemType GetElem(SqList* L, int index)
{
    if (index < 0 || index >= L->length || L->length == 0)
    {
        printf("GetElem failed\n");
        exit(EXIT_FAILURE);
    }
    else
    {
        printf("GetElem successed\n");
    }
    return ((L->elem)[index]);
}


int ListInsert(SqList* L, int index, ElemType* elem)
{
    if (L->length >= MAXSIZE|| index < 0 || index >= L->length)
    {
        printf("Insert failed!\n");
        exit(EXIT_FAILURE);
    }
    //插入时,把index后面所有数据往后面移动,但是我们不能从前移动,应该从后移动
    for (int i = L->length - 1; i >= index + 1; i--)
    {
        L->elem[i + 1] = L->elem[i];
    }
    //后移完毕,插入,len+1
    L->elem[index] = *elem;
    L->length++;
    printf("Insert successed!\n");
    return 0;
}

int ListDelete(SqList* L, int index)
{
    if (L->length ==0 || index < 0 || index >= L->length)
    {
        printf("Delete failed!\n");
        exit(EXIT_FAILURE);
    }

    //删除后我们从前往前移动
    for (int i = index + 1; i <= L->length; i++)
    {
        L->elem[i - 1] = L->elem[i];
    }
    L->length--;
    printf("Delete successed!\n");
}

3.5 LocateElem, PriorElem, NextElem

以下几个函数的编写需要函数指针,回调函数的知识,如果您不了解,可以去了解以下,又或者说忽略这3个函数,因为他们好像实际作用是比较低的。

//int (*cmp)(void* e1, void* e2)这是一个函数指针,
//因为我在设计时我并不知道你要传什么样的数据类型给我,所以我对你传过来的数据类型是要怎么比较一无所知
//比如我这里的Person,他应该怎么比较呢?是比较姓名?还是age?还是什么方法?
//所以你得自己写一个函数,然后传到这里来
//需要注意的是,你写的函数返回和参数列表必须跟我这里一致

int LocateElem(SqList* L, ElemType* e, int (*cmp)(void* e1, void* e2))
{
    if (L->length == 0)
    {
        printf("Locate failed!\n");
        exit(EXIT_FAILURE);
    }
    //找位置
    int i;
    for (i = 0; i < L->length; i++)
    {
        if (cmp((*L).elem, e) == 0)
        {
            break;
        }
    }
    return i;
}

ElemType PriorElem(SqList* L, ElemType* e, int (*cmp)(void* e1, void* e2))
{
    if (L->length == 0)
    {
        printf("PriorElem failed\n");
        exit(EXIT_FAILURE);
    }
    int i;
    for (i = 0; i < L->length; i++)
    {
        if (cmp(e, L->elem) == 0||i!=0)
        {
            return L->elem[i - 1];
        }
    }
    printf("PriorElem Not Found!\n");
}

ElemType NextElem(SqList* L, ElemType* e, int (*cmp)(void* e1, void* e2))
{
    if (L->length == 0)
    {
        printf("PriorElem failed\n");
        exit(EXIT_FAILURE);
    }
    int i;
    for (i = 0; i < L->length; i++)
    {
        if (cmp(e, L->elem) == 0 || i != 0)
        {
            return L->elem[i + 1];
        }
    }
    printf("PriorElem Not Found!\n");
}

3.6 分文件

如果您想直接使用,您可以访问以下网站获取头文件,函数定义,以及调试文件。
https://github.com/hastenboom/datastruct/tree/main/1%20%E7%BA%BF%E6%80%A7%E8%A1%A8/1.1%20%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8/C

4 方法的实现(C++)

在C++这里,我们可以使用类模板来实现以上功能,设计思路如下,构造函数代替Init,析构函数代替Delete,然后将C中的函数改造成面向对象中的成员函数形式即可。

#pragma once
#include<iostream>
using namespace std;

#define MAXSIZE 100


typedef struct
{
	int age;
	float weight;
}Person;

template<class T>
class SqList
{
private: 
	T* elem;
	int length;

public:
	//这个只是为了测试Person这个数据类型用的,具体类型使用时,这里是要修改的。
	friend ostream& operator<<(ostream& os, SqList& L)
	{
		for (int i = 0; i < L.length; i++)
		{
			cout << "element:" << i << ", age: " << L.elem[i].age << ", weight: " << L.elem[i].weight<<endl;
		}
		return os;
	}


	SqList()
	{
		elem = new T[MAXSIZE];
		if (!elem)
		{
			perror("Init Error: ");
			exit(EXIT_FAILURE);
		}
		length = 0;
		cout << "Initialization Successed!" << endl;
	}
	~SqList()
	{
		if (!elem)
		{
			perror("Delete Error: ");
			exit(EXIT_FAILURE);
		}
		delete[]elem;
		elem = NULL;
		length = 0;
		cout << "List is deleted!" << endl;
	}
	int LocateElem(T* e, int (*cmp)(void* e1, void* e2))
	{
		if (length == 0)
		{
			printf("Locate failed!\n");
			exit(EXIT_FAILURE);
		}
		//找位置
		int i;
		for (i = 0; i < length; i++)
		{
			if (cmp(&(elem[i]), e) == 0)
			{
				cout << "Location found: " << i << endl;
				return i;
			}
		}
		printf("No such elem!\n");
		return -1;
	}

我没有复制粘贴所有的方法实现,因为跟C版本大同小异,如果您需要完整版本以及我的调试文件,请到以下网址查看。
https://github.com/hastenboom/datastruct/tree/main/1%20%E7%BA%BF%E6%80%A7%E8%A1%A8/1.1%20%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8/cpp

5 一些算法的分析

5.1 LocateElem

虽然我写的函数用了函数指针让他能适应更广泛的处理,但是分析是一致的,并没有增加O。
在这里插入图片描述

5.2 ListInsert

在这里插入图片描述
假设对于插入任意位置都是等可能的,那么这个可能性是:
1 n + 1 \frac{1}{n+1} n+11
对于每个元素来说,在元素1处插入,则需要往后移n位;元素2处,n-1位……在元素n处,移动0位。于是有

1 n + 1 ∑ i = 0 n i = n 2 = O ( n ) \frac{1}{n+1}\sum_{i=0}^{n}i = \frac{n}{2}=O(n) n+11i=0ni=2n=O(n)

5.3 ListDelete

分析跟5.2差不多,假设等可能删除元素,那么有n个元素就是1/n的概率,那么删除第1个元素,需要移动n-1位;2,n-2位;n,0位,所以所求平均时间复杂度位:
1 n ∗ ∑ i = 0 n − 1 i = n + 1 2 − 1 = O ( n ) \frac{1}{n}*\sum^{n-1}_{i=0}i = \frac{n+1}{2}-1 = O(n) n1i=0n1i=2n+11=On

6 STL下的vector

为了以后方便统一使用这个东西,而不是每个人再用时造一个,有必要使用STL里面的一些内容。

vector其实就是这种顺序存储的直接对应。

6.1 初始化

  1. vectora(10);创建一个int数组,元素数量为10
  2. vector a(10,1); //定义了10个整型元素的向量,且给出每个元素的初值为1
  3. vector a(b); //用b向量来创建a向量,整体复制性赋值
  4. vector a(b.begin(),b.begin+3); //定义了a值为b中第0个到第2个(共3个)元素
  5. int b[7]={1,2,3,4,5,9,8};
    vector a(b,b+7); //从数组中获得初值

6.2 部分其余的成员函数

  1. a.back();//返回最后一个元素

  2. a.front();//第一个元素

  3. a[j];//当j合法,返回第j个元素,随机存储;

  4. a.clear();//清空,代替前面的ClearList

  5. a.empty();//是否为空,代替前面的isEmpty

  6. a.pop_back();
    a.push_back(5);//栈操作,pop删除最后的元素,push在最后加上5这个元素

  7. a.insert(a.begin()+1,5); //在a的第1个元素(从第0个算起)的位置插入数值5,如a为1,2,3,4,插入元素后为1,5,2,3,4

    a.insert(a.begin()+1,3,5); //在a的第1个元素(从第0个算起)的位置插入3个数,其值都为5

    a.insert(a.begin()+1,b+3,b+6); //b为数组,在a的第1个元素(从第0个算起)的位置插入b的第3个元素到第5个元素(不包括b+6),如b为1,2,3,4,5,9,8 ,插入元素后为1,4,5,9,2,3,4,5,9,8

  8. a.erase(a.begin()+1,a.begin()+3); //删除a中第1个(从第0个算起)到第2个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+ 3(不包括它)

  9. a.size();//代替ListLen

6.3 一些算法

  1. sort(a.begin(),a.end());
  2. reverse(a.begin(),a.end());
  3. find(a.begin(),a.end(),10);//部分代替LocateElem,如要实现LocateElem那种放具体比较函数的还是需要自己写

6.4 应用

1 把两个集合合并成一个新的集合,如v1 = [1,2,3,4]; v2 = [3,4,5,6],合并后的v3 = [1,2,3,4,5,6];
注:因为集合的定义要求元素是唯一的,所以在v1的元素中就必须是唯一的,不能重复。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <class T>
vector<T> combine(vector<T> &v1, vector<T> &v2)
{
    vector<T> tmp(v1.begin(), v1.end());
    for (auto i : v2)
    {
        for (auto j : v1)
        {
            if (i == j)
            {
                continue;
            }
            if (j == v1.back())
            {
                tmp.push_back(i);
            }
        }
    }
    return tmp;
}

int main()
{
    vector<int> v1{1, 2, 3, 4};
    vector<int> v2{4, 5, 6};
    vector<int> v3 = combine(v1, v2);
    for (auto i : v3)
    {
        cout << i << endl;
    }
    system("pause");
    return 0;
}

2 请计算一个稀疏的多项式,他可能长这样
3 x 0 + 6 x 2 + 7 x 5 + x 1000 3x^0+6x^2+7x^5+x^{1000} 3x0+6x2+7x5+x1000

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;

template <class T>
struct Poly
{
    T coe;
    T pow;
};

template <class T>
T computePoly(vector<Poly<T>> &vp, T x = 0)
{
    T sum = 0;
    for (auto i : vp)
    {
        sum = sum + i.coe * pow(x, i.pow);
    }
    return sum;
}

int main()
{

    vector<Poly<int>> v1{{3, 0}, {6, 2}, {7, 5}, {1, 1000}};
    cout << computePoly(v1, 1) << endl;
    system("pause");
    return 0;
}

7 对于实现的更新

如果想要在初始化时就实现像vector<Poly<int>> v1{{3, 0}, {6, 2}, {7, 5}, {1, 1000}};这样的一次性多次输入,需要使用C++11的initializer_list来对构造函数进行改造:

#include <iostream>
#include <initializer_list>
using namespace std;
#define MAXSIZE 100

template <class T>
class SqList
{
private:
    T *elem;
    int length;

public:
    SqList(initializer_list<T> initList)
    {
        elem = new T[MAXSIZE];
        length = 0;
        for (auto i : initList)
        {
            *(elem + length) = i;
            length++;
        }
    }

    int empty()
    {
        return length == 0 ? 1 : 0;
    }
    void print()
    {
        for (int i = 0; i < length; i++)
        {
            cout << *(elem + i) << ' ';
        }
        cout << endl;
    }

    
};

int main()
{
    SqList<int> L{1, 2, 3, 4, 5, 6};
    cout << L.empty() << endl;
    L.print();
    system("pause");
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值