数据结构与算法

文章目录

数据结构

概述

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

分类

从关注的维度看,数据结构可以划分为数据的逻辑结构和物理结构,同一逻辑结构可以对应不同的存储结构。逻辑结构反映的是数据元素之间的逻辑关系,逻辑关系是指数据元素之间的前后间以什么形式相互关联,这与他们在计算机中的存储位置无关。
逻辑结构包括:

  1. 集合:只是扎堆凑在一起,没有互相之间的关联
  2. 线性结构:一对一关联,队形
  3. 树形结构:一对多关联,树形
  4. 图形结构:多对多关联,网状

数据物理结构指的是逻辑结构在计算机存储空间中的存放形式(也称为存储结构)。一般来说,一种数据结构的 逻辑结构根据需要可以表示成多种存储结构,常用的存储结构有:

  1. 顺序存储:用一组地址连续的存储单元依次存储集合的各个数据元素,可随机存取,但增删需要大批移动
  2. 链式存储:不要求连续,每个节点都由数据域和指针域组成,占据额外空间,增删快,查找慢需要遍历
  3. 索引存储:除建立存储结点信息外,还建立附加的索引表来标识结点的地址。检索快,空间占用大
  4. 哈希存储:将数据元素的存储位置与关键码之间建立确定对应关系,检索快,存在映射函数碰撞问题

程序中常见的数据结构

  1. 数组(Array):连续存储,线性结构,可根据偏移量随机读取,扩容困难
  2. 栈( Stack):线性存储,只允许一端操作,先进后出,类似水桶 队列(Queue):类似栈,可以双端操作。先进先出,类似水管
  3. 链表( LinkedList):链式存储,配备前后节点的指针,可以是双向的
  4. 树( Tree):典型的非线性结构,从唯一的根节点开始,子节点单向执行前驱(父节点)
  5. 图(Graph):另一种非线性结构,由节点和关系组成,没有根的概念,互相之间存在关联
  6. 堆(Heap):特殊的树,特点是根结点的值是所有结点中最小的或者最大的,且子树也是堆
  7. 散列表(Hash):源自于散列函数,将值做一个函数式映射,映射的输出作为存储的地址

算法

算法指的是基于存储结构下,对数据如何有效的操作,采用什么方式可以更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。一般涉及的操作有以下几种:

  1. 检索:在数据结构里查找满足一定条件的节点。
  2. 插入:往数据结构中增加新的节点,一般有一点位置上的要求。
  3. 删除:把指定的结点从数据结构中去掉,本身可能隐含有检索的需求。
  4. 更新:改变指定节点的一个或多个字段的值,同样隐含检索。
  5. 排序:把节点里的数据,按某种指定的顺序重新排列,例如递增或递减。

复杂度

时间复杂度

简单理解,为了某种运算而花费的时间,使用大写O表示。一般来讲,时间是一个不太容易计量的维度,而为了计 算时间复杂度,通常会估计算法的操作单元数量,而假定每个单元运行的时间都是相同的。因此,总运行时间和算 法的操作单元数量一般来讲成正比,最多相差一个常量系数。一般来讲,常见时间复杂度有以下几种:

1)常数阶O(1):时间与数据规模无关,如交换两个变量值
int i=1,j=2,k;
k=i;i=j;j=k;

2)线性阶O(n):时间和数据规模呈线性,可以理解为n的1次方,如单循环里的操作
for(i=1;i<=n;i++){
       do();
}
3)k次方阶O(nk):执行次数是数量的k次方,如多重循环,以下为2次方阶实例
 for(i=1;i<=n;i++){
     for(j=1;j<=n;j++){
		do(); 
	}
}
4)指数阶O(2n):随着n的上升,运算次数呈指数增长
   for(i=1;i<= 2^n;i++){
       do();
   }
5)对数阶O(log2n):执行次数呈对数缩减,如下
     for(i=1;i<=n;){
       i=2^i;
		do(); 
	} 
6)线性对数阶O(nlog2n):在对数阶的基础上,进行线性n倍乘积
for(i=1;i<=2^n;i++){
    for(j=1;j<=n;j++){
		do(); 
	}
}

7)总结: 时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<...(nk)<Ο(2n)             

空间复杂度

与时间复杂度类似,空间复杂度是对一个算法在运行过程中占用内存空间大小的度量。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的辅助空间。而空间复杂度主要指的是这部分空间的量级。

固定空间:主要包括指令空间、常量、简单变量等所占的空间,这部分空间的大小与运算的数据多少无关,属于静态空间。

可变空间:主要包括运行期间动态分配的临时空间,以及递归栈所需的空间等,这部分的空间大小与算法有很大关系。

同样,空间复杂度也用大写O表示,相比时间复杂度场景相对简单,常见级别为O(1)和O(n),以数组逆序为例,两 种不同算法对复杂度影响如下:

  1. O(1):常数阶,所占空间和数据量大小无关。
//定义前后指针,和一个临时变量,往中间移动 //无论a多大,占据的临时空间只有一个temp int[] a={1,2,3,4,5};
int i=0,j=a.length‐1;
   while (i<=j){
       int temp = a[i];
       a[i]=a[j];
       a[j]=temp;
       i++;
       j‐‐;
}  
  1. O(n):线性阶,与数据量大小呈线性关系。
//定义一个和a同等大小的数组b,与运算量a的大小呈线性关系 //给b赋值时,倒序取a
int[] a={1,2,3,4,5};
int[] b=new int[a.length];
   for (int i = 0; i < a.length; i++) {
       b[i]=a[a.length‐1‐i];
}
    

总结

对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。时间复杂度低可能借助占用大的存储空间来弥补,反之,某个算法所占据空间小,那么可能就需要占用更多的运算时间。两者往往需要达到一种权衡。在特定环境下的业务,还需要综合考虑算法的各项性能,如使用频率,数据量的大小,所用的开发语言,运行的机器系统等。两者兼顾权衡利弊才能设计出最适合当前场景的算法。

文章目录


前言

分享一下最近面试遇到过的简单算法题,本篇介绍的是如何判断链表有环相关的3种方法。
本篇需要一定的数据结构和复杂度基础。

算法题

有一个单向链表,链表中有可能出现环,如下图所示。
如何用程序判断该链表是否为环链表?
在这里插入图片描述

遍历链表

从头结点开始,依次遍历单链表中的每一个节点,每遍历一个新节点,就从头检查新节点之前的所有节点,用新节点和此节点之前所有的节点依次做比较,如果发现新的节点和之前的某个节点相同,则说明该节点被遍历过2次,则可判断为有链表有环。

有点类似于冒泡排序的意思,则这个解法时间复杂度为 O(n2),空间复杂度为 O(1).。
效率比较低,不推荐,一般面试官也不会太满意的。

这个解法的代码太简单了,就不附上代码了。

HashSet

创建一个hashSet集合,遍历链表将每个节点作为key用来存储到set中,每遍历一个新节点,都用新节点和set集合中存储的节点做比较,如果发现存在与节点相同的key,则该链表为环形链表。

这个解法就是典型用空间换时间,时间复杂度为O(n),空间复杂度为O(n),相对上一个解法,效率高了很多。

但是很遗憾这个也不是面试官想要的解法(面试官就是事妈😂)。

这个解法的代码太简单了,就不附上代码了。

快慢指针

创建2个指针P1和P2,开始时都指向链表的表头节点,然后开始遍历链表,让指针向后移动,其中P1为慢指针每次向后移动1个节点,P2位快指针每次向下移动2个节点,最后比较这2个指针指向的节点是否相同,如果相同则该链表为环形链表。

以上面的链表图为例,模拟一下这个解法的图示:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如上图所示,指针向下移动4次就会指向同一个位置。

这种解法的时间复杂度是O(n),空间复杂度是O(1),也是最优的解法,面试官想要的也是这种解法。

快慢指针解法代码

代码如下(示例):

/**
* 判断链表是否有环
*/
public static boolean isCycle(Node node){
	//2个指针初始化都是在链表的表头节点
	Node p1 =node;
	Node p2 =node;
	//遍历链表  链表还存在下一个节点
	while(p2!=null&&p2.next!=null){
		//p1是慢指针,每次向下移动1个节点
		p1 = p1.next;
		//p2是快指针,每次向下移动2个节点
		p2=p2.next.next;
		//如果2个指针指向的节点相同那么就是环形链表
		if(p1==p2){
			return true;
		}
	}
	return false;
}


/**
* 链表节点
*/
private static class Node{
	int data;
	Node next;
	Node(int data){
		this.data=data;
	}
}

总结

其实这是一道非常基础的算法题,我在leetcode上面也刷到了这道题,面试概率也会相对较高。
算法一般是从时间复杂度和空间复杂度这2个维度去衡量,在算法里面最常见优化效率的方法就是用空间来换时间。

文章目录


前言

复盘一下最近面试遇到简单关于算法的面试题,本篇介绍的是大数相加的算法实现。

题目

面试官: 请你说一下如何实现2个大数相加的业务?
面试者:这个简单,用long接收数字,再计算不就好了。
面试官:那如果是这个大数连long都无法装下呢,如100位的大数。
面试者:会不会是题目出错了?
面试官:题目没有错,今天就先面试到这里回去等通知吧。
面试者:###xxxx。。。

解题思路

  1. 通过比较2个大数的字符长度来确认数组的长度 :maxLength+1;
  2. 将2个大数字符串通过字符串截取倒序的分表放到2个数组中,如 3248 放到数组中就是 {8,4,2,3} ;
  3. 创建一个结果数组,然后遍历数组,用2个数组相加,当前位数大于等于10则减10下一位进1 ;
  4. 倒序遍历结果数组,将结果拼接到stringbuilder中(要注意找到数组中真正的首位数字)。

代码实现

代码如下(示例):

    /**
     * 大数相加实现  用2个数组来相加
     * @param bigNumberA 大数a
     * @param bigNumberB 大数b
     * @return
     */
    public static String add(String bigNumberA,String bigNumberB) {
        //确认出最大的数组长度
        int maxLength = bigNumberA.length() > bigNumberB.length() ? bigNumberA.length() : bigNumberB.length();

        //将大数A拆成数字放到数组中 倒序插入数组中 个位数在数组的第一位
        int[] arrayA = new int[maxLength + 1];
        for (int i=0;i<bigNumberA.length();i++) {
            arrayA[i] = Integer.parseInt(bigNumberA.substring(bigNumberA.length()-1-i,bigNumberA.length()-i));
        }

        //将大数B拆成数字放到数组中 倒序插入数组中 个位数在数组的第一位
        int[] arrayB = new int[maxLength + 1];
        for (int i = 0; i < bigNumberB.length(); i++) {
            arrayB[i] = Integer.parseInt(bigNumberB.substring(bigNumberB.length()-1-i,bigNumberB.length()-i));
        }

        //创建一个计算结果的数组
        int[] result = new int[maxLength + 1];
        for(int i=0;i<result.length;i++){
            int temp = result[i];
            //同位数相加
            temp+=arrayA[i];
            temp+=arrayB[i];
            //如果大于等于10则下一位数进1
            if(temp>=10){
                temp =temp-10;
                result[i+1] =1;
            }
            result[i] =temp;
        }

        //定义一个是否找到首位的数字
        boolean findFirst = false;
        //将result数组转换为数字
        StringBuilder builder =new StringBuilder();
        for(int i=result.length-1;i>=0;i--){
            if(!findFirst){
                if(result[i]==0){
                    //首位数字为0都直接结束本次的循环
                    continue;
                }
                //找到首位数字直接赋值为true
                findFirst = true ;
            }
            builder.append(result[i]);
        }

        return builder.toString();
    }

测试

    public static void main(String[] args) {
       String str = BigDataAddition.add("2339831","8323623");
        System.out.println(str);
    }

测试结果:
在这里插入图片描述

总结

算法题难得是解题的思路,如果思路有了,代码的实现并不难,大家在刷leetcode时,不要为了刷题而刷题,尽量去理解解题的思路。

面试

25 篇文章 0 订阅

订阅专栏

文章目录


前言

本篇介绍算法的五大算法思想,如果还不了解算法的基础概念请看一下这篇文章:

初识数据结构和算法

分而治之

把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题,直到最后子问题小到可 以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序, 归并排序),傅立叶变换(快速傅立叶变换),大数据中的MR,现实中如汉诺塔游戏。

分治法对问题有一定的要求:

  1. 该问题缩小到一定程度后,就可以轻松解决
  2. 问题具有可拆解性,不是一团无法拆分的乱麻
  3. 拆解后的答案具有可合并性。能组装成最终结果
  4. 拆解的子问题要相互独立,互相之间不存在或者很少有依赖关系

动态规划

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他。依次解决各子问题,最后一个子问题就是初始问题的解。

与分治法最大的不同在于,分治法的思想是并发,动态规划的思想是分步。该方法经分解后得到的子问题往往不是互相独立的,其下一个子阶段的求解往往是建立在上一个子阶段的解的基础上。动态规划算法同样有一定的适用性场景要求:

  1. 最优化解:拆解后的子阶段具备最优化解,且该最优化解与追踪答案方向一致
  2. 流程向前,无后效性:上一阶段的解决方案一旦确定,状态就确定,只会影响下一步,而不会反向影响
  3. 阶段关联:上下阶段不是独立的,上一阶段会对下一阶段的行动提供决策性指导。这不是必须的,但是如果具备该特征,动态规划算法的意义才能更大的得到体现

贪心算法

同样对问题要求作出拆解,但是每一步,以当前局部为目标,求得该局部的最优解。那么最终问题解决时,得到完整的最优解。也就是说,在对问题求解时,总是做出在当前看来是最好的选择,而不去从整体最优上加以考虑。从这一角度来讲,该算法具有一定的场景局限性:

  1. 要求问题可拆解,并且拆解后每一步的状态无后效性(与动态规划算法类似)
  2. 要求问题每一步的局部最优,与整体最优解方向一致。至少会导向正确的主方向。

回溯算法

回溯算法实际上是一个类似枚举的搜索尝试过程,在每一步的问题下,列举可能的解决方式。选择某个方案往深度 探究,寻找问题的解,当发现已不满足求解条件,或深度达到一定数量时,就返回,尝试别的路径。回溯法一般适 用于比较复杂的,规模较大的问题。有“通用解题法”之称:

  1. 问题的解决方案具备可列举性,数量有限
  2. 界定回溯点的深度。达到一定程度后,折返。

分支限界

与回溯法类似,也是一种在空间上枚举寻找最优解的方式。但是回溯法策略为深度优先。分支法为广度优先。分支
法一般找到所有相邻结点,先采取淘汰策略,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从存活表
中选择一个结点作为下一个操作对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ariel小葵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值