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

目录

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]);
	}
}

题外话

今年很多人都在讨论一个问题:就业形势险峻,毕业生们怎么办?

错过了春招,秋招竞争激励,现在投了几十份简历却还都石沉大海,22/23届同学烦得头都快秃了。

在这里插入图片描述
然而有人早就突出重围、拿下几个offer。

表弟疫情在家就没闲着,他说“自己专业技能不够硬,得多学些东西傍身才行。”

抱着这样的想法,他开始在家里学习Python。因为他在逛各大招聘网站的时候发现:

现在很多大厂的招聘JD,

很多都直接写明“优先录取Python的人”——自动化办公、高效工作。

在这里插入图片描述
在这里插入图片描述
什么运营、会计、律师等看似和Python无关的人都在学Python,而且在这方面的人才缺口极大!

编程Python已经不仅仅是职场人士需要具备的技能,

在校大学生和毕业生更需要它来为自己的简历加分,是一块大厂的敲门砖。

曾经在一个帖子上看到有「麦肯锡牛人」称,

如果有人能回答Python的相关问题,将很乐意提供内推机会。
在这里插入图片描述

如今是一个大数据的时代,Python 在行为收集和数据分析,信息采集等方面的应用已经非常非常普遍,早就不是程序员的专属技能了。就像 office 一样,是Python 已经成为了进入职场的必备技能。不是很意外,但这就是正在发生的大趋势。
在这里插入图片描述

Python的特点

1.需求大:百度、新浪、搜狐、淘宝、腾讯QQ等大部门的互联网相关企业都在利用Python,对Python的人才需求很大。

2.开展空间广:在无孔不入的互联网使用情况下,人工智能、大数据等领域非常适合Python的发展,这也就阐明了挑选进修Python将会有很不错的发展空间。

3.简单易学:小学生也可以上手学习的计算机语言。举个例子一个程序用C语言需要1000行的代码,用JAVA需要写100行,但是如果用Python你只需要20行,语法很简洁。

Python的就业方向有哪些?
在这里插入图片描述

Python岗位薪资水平如何?
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

Python如何学习

今天只要你给我的文章点赞,我私藏的Python学习资料一样免费共享给你们,来看看有哪些东西。

Python学习大礼包

在这里插入图片描述

Python入门到精通背记手册

在这里插入图片描述
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
👉CSDN大礼包:《python入门&进阶学习资源包》免费分享

Python安装包

在这里插入图片描述

Python爬虫秘籍

在这里插入图片描述

Python数据分析全套资源

在这里插入图片描述
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
👉CSDN大礼包:《python入门&进阶学习资源包》免费分享

Python实现办公自动化全套教程

在这里插入图片描述

Python面试集锦和简历模板

在这里插入图片描述
在这里插入图片描述

Python副业兼职路线

在这里插入图片描述

资料领取

上述这份完整版的Python全套学习资料已经上传CSDN官方,朋友们如果需要可以微信扫描下方CSDN官方认证二维码 即可领取↓↓↓

CSDN大礼包:《python入门&进阶学习资源包》免费分享

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值