复杂度?顺序表?看这一篇就够了!从这里开始你的数据结构与算法之旅吧!

目录

1.复杂度

(1)时间复杂度

(2)空间复杂度

2.顺序表

(1)顺序表的概念

(2)顺序表的实现


        在本篇文章中,笔者将对时空复杂度和顺序表的相关内容进行详细讲解。如果你正在考虑学习数据结构与算法,不妨从这里开始吧!阅读本篇文章之前,你需要对C语言的语法和一些简单算法有基本了解。那么,Let's go

一.复杂度

         算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

1.时间复杂度

        时间复杂度用于衡量一个算法运行的快慢。它指的并不是运行一个算法需要花费的确切时间(决定这个时间的因素众多,如书写程序的语言、编译产生的机器代码质量、机器执行指令的速度等。因此在机器执行程序之前,我们并不能知道这个时间),而是算法中的基本操作的执行次数。时间复杂度的表示采用大O的渐进表示法,而推导大O阶的规则如下:

(1).用常数1取代运行时间中的所有加法常数。
(2).在修改后的运行次数函数中,只保留最高阶项。
(3).如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

        看完了上面的介绍,你是不是有一种云里雾里的感觉?没关系,下面让我们通过几个例子来练习时间复杂度的计算吧!

例子1

void Func1(int N)
{
    int count = 0;
    for (int i = 0; i < N ; ++ i)
    {
        for (int j = 0; j < N ; ++ j)
        {
            ++count;
        }
    }
    for (int k = 0; k < 2 * N ; ++ k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
}

如图,在上面的代码中,可以看到++count一共被执行了({N_{}}^{2}+2N+10)次。但是根据规则(2),可知Func1的时间复杂度为O({N_{}}^{2})。

例子2

void Func2(int N)
{
    int count = 0;
    for (int k = 0; k < 2 * N ; ++ k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
}
printf("%d\n", count);

如上代码中,“++count”一共被执行(2N+10)次。根据规则(3),它的时间复杂度为O(N)

例子3

void Func4(int N)
{
    int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
        ++count;
    }
}
printf("%d\n", count);

如上代码中,“++count”一共被执行100次。那么根据规则(1),它的时间复杂度为O(1).

        看了以上三个例子,相信你对时间复杂度已经有了一定的了解吧!下面笔者给出一些常见算法的时间复杂度,具体计算作为练习请读者完成,有不懂的地方欢迎在评论区提问哦~~

注:今后logN表示以2为底N的对数。

三种算法的时间复杂度
冒泡排序O({N_{}}^{2})
二分查找O(logN)
阶乘递归O(N)

2.空间复杂度

        空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。它计算的是变量的个数,也采用大O的渐进表示法,主要通过函数在运行时候显式申请的额外空间来确定。下面让我们通过几个例子来揭开它的神秘面纱吧!

例子1(冒泡排序)

void BubbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
    {
        int exchange = 0;
        for (size_t i = 1; i < end; ++i)
        {
            if (a[i-1] > a[i])
            {
                Swap(&a[i-1], &a[i]);
                exchange = 1;
            }
        }
        if (exchange == 0)
        break;
    }
}

 注:此处的Swap表示封装的一个交换两变量的函数。

冒泡排序中额外创建的变量有size_t end,int exchange,size_t i(a数组不计入,因为它不是额外开辟的空间)为常数个,因此空间复杂度为O(1)。

例子2(返回斐波那契数列前n项)

long long* Fibonacci(size_t n)
{
    if(n==0)
        return NULL;
    long long * fibArray = (long long *)malloc((n) * sizeof(long long));
    fibArray[0] = 1;
    fibArray[1] = 1;
    for (int i = 2; i < n ; ++i)
    {    
        fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
    }
    return fibArray;
}

 上述代码中开辟了一个共n个元素的数组,还有一些零散的随机变量,因而空间复杂度为O(N)。

例子3(阶乘递归)

long long Fac(size_t N)
{
    if(N == 0)
        return 1;
    return Fac(N-1)*N;
}

上述代码中,每次递归调用的空间复杂度为O(1),一共调用N次,因而空间复杂度为O(N)

注:熟悉函数栈帧的读者也可以从函数栈帧的角度考虑:一共创建了N个函数栈帧。

例子4.(递归计算斐波那契数列第n项)

long long Fib(size_t N)
{
    if(N<3)
        return 1;
    return Fib(N-1)+Fib(N-2);
}

上述代码中Fib函数被调用的次数为{2_{}}^{N-1}-1,但空间复杂度应该为O(N)。这是因为:

        每轮调用的两个Fib函数先后创建,但是它们共用一个栈帧!!!

看了上面的例子,想必你对时间复杂度和空间复度已经有了初步了解吧!那么不要留恋,让我们进入下一部分——顺序表的学习吧!

二.顺序表

1.何为顺序表?

        顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改。

2.顺序表的实现

(1)静态顺序表

       设置静态顺序表的容量是个大问题,设置小了吧不够用,设置大了吧会造成浪费。因为它有这个缺陷,不太实用,所以我们不实现它。

(2)动态顺序表

        动态顺序表可以按需开辟空间,更加方便。那么接下来就让我们实现动态顺序表吧!
        顺序表应该包含:一个指向动态开辟空间的指针一个表示当前数据个数的变量和一个表示数组容量的变量。同时,为了避免以后数组数据类型改变带来的不便,我们可以对数据类型进行重定义,这样发生上述情况时就只需要改动一个地方啦~~同时我们应该提供一些函数接口,以改变数组的数据。以下是我们应该提供的函数接口和结构体定义:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* a;
	size_t N;
	size_t capacity;
}SL;
//初始化
void SLInit(SL*);
//销毁
void SLDestroy(SL*);
//检查容量
void checkCapicity(SL*);
//尾插
void SLPushBack(SL*, SLDataType);
//尾删
void SLPopBack(SL*);
//头插
void SLPushFront(SL*, SLDataType);
//头删
void SLPopFront(SL*);
//插入数据
void SLInsert(SL*, int, SLDataType);
//根据位置删除数据
void SLEraseByPos(SL*, int);
//根据值删除数据
void SLEraseByVal(SL*, SLDataType);
//寻找数据
int SLFind(SL*, SLDataType,int);
//打印
void SLPrint(SL*);

接下来让我们一一实现吧!

初始化——

void SLInit(SL* ps)
{
	assert(ps);

	ps->a = NULL;
	ps->capacity = 0;
	ps->N = 0;
}

销毁——

void SLDestroy(SL* ps)
{
	if (ps->a)
	{
		free(ps->a);
		ps->a = NULL;
		ps->N = ps->capacity = 0;
	}
}

注意:这里不要free(ps)!因为它不是动态开辟的空间!另外,可能有读者担心会出现ps->a为NULL但是N与capicity不为0的情况。实际上这是不会发生的,因为ps->a为NULL意味着数组已经是空的。

检查容量——

void checkCapicity(SL* ps)
{
	assert(ps);
	if (ps->capacity == ps->N)
	{
		int newCapicity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapicity * sizeof(SLDataType));
		if (!tmp)
		{
			perror("realloc failed");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newCapicity;
	}
}

空间满了以后要开辟空间。这里为了防止开辟空间失败ps->a接收到空指针,我们要先把realloc返回的指针赋给tmp。若开辟失败,exit(-1)异常退出程序;若开辟成功,则把tmp赋给ps->a。

尾插(在尾部插入数据)——

void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);
	checkCapicity(ps);
	ps->a[ps->N] = x;
	ps->N++;
}

在插入数据前,我们要保证ps不为NULL,否则会产生对空指针解引用的问题,因此用assert断言。在尾插之前还应该检查容量。

尾删(在尾部删除数据)——

void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->N > 0);
	ps->N--;
}

第三行的作用是防止数组已经为空时仍然删除数据。

头插(在头部插入数据)——

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	checkCapicity(ps);
	int end = ps->N - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	ps->a[0] = x;
	ps->N++;
}

图解:

 头删(删除头部数据)——

void SLPopFront(SL* ps)
{
	assert(ps && ps->N > 0);
	int begin = 1;
	while (begin < ps->N)
	{
		ps->a[begin - 1] = ps->a[begin];
		begin++;
	}
	ps->N--;
}

从前往后,依次覆盖数据。

插入数据——

}
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos < ps->N&& pos >= 0);
	checkCapicity(ps);
	int end = ps->N - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		end--;
	}
	ps->a[pos] = x;
	ps->N++;
}

与头插类似,只不过是从pos位置开始。

删除数据——

void SLEraseByPos(SL* ps, int pos)
{
	assert(ps && pos >= 0);
	int begin = pos;
	while (begin < ps->N)
	{
		ps->a[begin] = ps->a[begin + 1];
		begin++;
	}
	ps->N--;
}

与头删类似,只不过是从pos开始。

寻找数据——

int SLFind(SL* ps, SLDataType x, int begin)
{
	assert(ps);
	assert(begin < ps->N&& begin >= 0);
	int i = 0;
	for (i = begin; i < ps->N; i++)
	{
		if (ps->a[i] == x)
			return i;
	}
	return -1;
}

根据值删除数据——

void SLEraseByVal(SL* ps, SLDataType x)
{
	int ret = SLFind(ps, x, 0);
	while (ret != -1)
	{
		SLEraseByPos(ps, ret);
		ret = SLFind(ps, x, ret);
		
	}
}

打印数据——

void SLPrint(SL* ps)
{
	assert(ps);
	int i = 0;
	for (i = 0; i < ps->N; i++)
	{
		printf("%d", ps->a[i]);
	}
}

好啦,以上就是本篇文章的全部内容。如果你觉得对你有所帮助,请一键三连支持一下作者哦!也欢迎各位批评指正,或是在评论区留下你的困惑,作者会给予解答的~~~~~

  • 14
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值