数据结构与算法

数据结构与基础算法笔记(一)

前言

由于个人原因,之前连载的Spring框架技术将在本年开始继续更新,有朋友刚好学习的话可以一起交流,这里结合我本学期的课程开设算法专栏,会包括面试出现的各种常见的算法。代码大多来自leetcode官网,配合自己的个人见解,如果错误或者不足的地方,敬请指正!

因为本节是数据结构最基础的部分,有一些大家很熟悉的基础点就不详细的赘述了。重点是算法题目和代码的质量。

数组

先看看数组在不同语言下的表现形式吧:

在这里插入图片描述

数组的缺陷:插入和删除功能困难,为了给加入的数组腾出来空间,需要进行数值空间的移动,耗费了很多时间,时间复杂度为O(n)级别。以下是java中封装的源码。

// 在数组最后位置添加元素
public boolean add(E e)
{
    // 操作的次数
	modCount++;
    // 确保当前数组的长度是可以添加元素的
	if (size == data.length)
	  ensureCapacity(size + 1);
	data[size++] = e;
	return true;
}

// 在确定的位置添加元素
public void add (int index, E e) 
{
    // 检查边界
    checkBoundInclusive(index);
    modCount++;
    if (size == data.length)
        ensureCapacity(size + 1);
    /* 
    	数组后半部分进行数据挪动
    	System.arraycopy(原数据,原地址,新数据,新地址,被移动数组长度)
    */
    if (index != size)
        System.arraycopy(data, index, data, index + 1, size - index);
    data[index] = e;
    size++;
}

// 确保数组size的长度足够,比较暴力,单纯new一个新的2倍的扩容。
public void ensureCapacity(int minCapacity)
{
    int current = data.length;
    
    if (minCapacity > current)
    {
        E[] newData = (E[]) new Objrct[Math.max(current * 2, minCapacity)];
        System.arraycopy(data, 0, newData. 0, size);
        data = newData;
    }
}

链表

链表的结构图如下

在这里插入图片描述

链表的种类有单链表、双向链表和循环链表,头指针为head,尾指针为tail。他对于插入和删除都非常方便,不需要有数值的移动,只需要做指针的位置的修改,配合开辟和释放相应的资源。

链表的结点定义

// 目前我写的代码 2023年2月26日
class Node {
	Node head;
	Node next;
	int size;
}

// 最朴素的链表实现
class LinkedList {
    // head of list 头指针
    Node head;
    
    /* Linked list Node */
    class Node {
        int data;
        Node next;
        
        // 构造方法
        Node (int d) { data = d; }
    }
}

// 结合泛型对链表进行实现,
public class LinkedList<AnyType> inplements Iterable<AnyType>
{
    private Node<AnyType> head;
    
    private static class Node<AnyType>
	{
        private AnyType data;
        private Node<AnyType> next;

        public Node (AnyType data, Node<AnyType> next)
        {
            this.data = data;
            this.next = next;
    	}
	}
}    

// java封装的LinkedList   注意java中的LikedList是一个双向链表的实现
// 这个是核心,有了对数据安全的考虑
public class LinkedList<T> extends AbstractSequentialList<T>
    implements List<T>, Deque<T>, Cloneable, Serializable
{
    // transient 通俗来说 这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
    transient Entry<T> first;
    transient Entry<T> last;
    transient int size = 0;
    
    // Node
    private static final class Entry<T>
    {
        T data; 
        Entry<T> next;
        Entry<T> previous;
        
        Entry(T data)
        {
            this.data = data; 
        }
    }
}

链表的增加和删除的时间复杂度O(1),相比于数组效率上提升很多,但是对于数组,如果要访问某个数据需要从头节点开始逐步遍历,时间复杂度O(n)

跳表

为了弥补链表在遍历操作上的不足,出现了跳表。它的核心思想是升维、空间换时间,这也是对于所有算法优化的基本思想。可能在本科的数据结构课程中很少提到它,但是它在实际的工程应用中还是比较常见的,其中比较熟悉的 RedisLRU Cache 就有它的应用。

在这里插入图片描述

有了索引之后,如果需要遍历原始链表的节点,从最高级的索引,可以减少遍历节点的个数,从而提高效率

跳表建立后,在有元素的增加和删除时,相应的索引表也需要改变,并不是像图中这样完整工整的结构。维护索引表也需要消耗非常庞大的复杂度。

索引表在面试中不会要求写代码,主要了解它的原理和一些工业的应用即可。

以下是Redis中跳表的应用:

(72 条消息) 为啥 redis 使用跳表(skiplist)而不是使用 red-black? - 知乎 (zhihu.com)

配套习题

算法练习的过程:

  1. 5-10分钟:读题思考
  2. 有思路:自己开始做和写代码;不然,马上看题解!
  3. 默写背诵、熟练
  4. 不参考资料,自己敲出来

题目来源leetcode283 移动零

题目描述:

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

解题思路:

  1. 顺序遍历,过程中记录 0 的个数,直至结束,数组最后元素设定为0;
  2. 重新开一个新的数组,是 0 就从数组后面插入,如果不是 0 就按顺序在新数组上插入;
  3. 在原来的数组中利用双指针法遍历整个数组就行。

解法:(题目简单,直接看代码吧)

  public void moveZeroes(int[] nums) {
      for (int i = 0; i < nums.length; i++) {
          if (nums[i] == 0) {
              int temp = i + 1;
              while (temp < nums.length && nums[temp] == 0)
                  temp++;
              if (temp < nums.length) {
                  nums[i] = nums[temp];
                  nums[temp] = 0;
              }
          }
      }
  }

题目来源P11. 盛最多水的容器 - 力扣(LeetCode)

题目描述:

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

  • 输入:[1,8,6,2,5,4,8,3,7]
  • 输出:49

在这里插入图片描述
这里提供两个版本的java解法,第二种方法代码风格更加精炼。

// 无论是嵌套循环的的遍历,还是左右向中间夹逼的思路都是非常常见的算法思路。
// 嵌套循环
public int maxArea(int[] height) {
    int left = 0;
    int right = height.length;
    int max = 0;

    // 这个方法也可以进行数组的遍历,但是不够简介
    for (int i = 0; i < height.length; i++) {
        // 左边界都是从 0 开始
        // 右边界可以一点一点的缩小
        right = height.length - i;
        left = 0;
        while (right < height.length) {
            int temp1 = right - left;
            int temp2 = Math.min(height[left], height[right]);

            if (temp1 * temp2 > max)
                max = temp1 * temp2;

            left++;
            right++;
        }
    }
    return max;
}


// 双指针法,向中间夹逼,代码简洁度非常高。
public int maxArea1(int[] height) {
    int max = 0;
    for (int i = 0, j = height.length - 1; i < j; ) {
        int minHeight = height[i] < height[j] ? height[i++] : height[j--];
        // 此处为什么需要有 +1, 上一步在执行之后默认会有向中间移动的操作。
        int area = (j - i + 1) * minHeight;
        max = Math.max(area, max);
    }
    return max;
}

python版本的代码:

def climbStairs(self, n: int) -> int:
    if n <= 2:
        return n

    a, b, c = 1, 2, 3
    while n > 3:
        a = b
        b = c
        c = a + b
        n -= 1
    return c

下节预告:虽然数组和链表比较简单,但题目灵活多变,打好基础,下一节是对应第一章的练习!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值