数据结构与算法学习笔记

数据结构与算法(C++版)学习笔记 先学后练2.先学经典,再去拓展延伸3. C/C++数据结构数组、链表、串、栈、队列…经典算法冒泡算法、BF算法… 阅读详情
AI摘要

本文深入探讨数据结构与算法的基础知识,涵盖数组、链表、队列、递归算法及排序方法,解析时间与空间复杂度,提供实用案例与代码实现。

数据结构与算法思维导图

数据结构指的是“一组数据的存储结构”,算法指的是“操作数据的一组方法”。
数据结构是为算法服务的,算法是要作用再特定的数据结构上的。

最常用的数据结构预算法:

  • 数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Tire树
  • 算法: 递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法

算法的复杂度

大O复杂度表示法

 公式:

我们通常用 T(n)来表示代码执行的时间,其中 n代表数据规模的大小。而 f(n)则表示所有代码执行次数的总和,它是一个关于 n的函数。

大O表示法的公式 T(n) = O(f(n))意在说明,代码的执行时间 T(n)与执行次数总和 f(n)成正比关系。

例如,假设某段代码的 f(n) = 2n + 2,那么它的 T(n)就可以表示为 O(2n+2)。另一段代码的 f(n)可能是 2n² + 2n + 3,其 T(n)则可表示为 O(2n²+2n+3)。

大O时间复杂度表示法并不精确计算代码真正的执行时间,而是形象地描绘出代码执行时间随数据规模 n增大而变化的速度趋势

。因此,它也被称为渐进时间复杂度(asymptotic time complexity)。

当数据规模 n变得非常大时(例如想象 n是 10000 甚至 100000),公式中的低阶项(如 +2n、+3)、常数项(如 2n中的 2)以及系数都对整个增长趋势的影响变得微乎其微,可以忽略不计

。我们只需要关注最高阶项,因为它主导了增长的趋势。

所以,上面两个例子用大O表示法简化记作:

  • T(n) = O(n)
  • T(n) = O(n²)

简单来说,大O表示法抓大放小,只关注最影响效率的主要矛盾。


时间复杂度分析法则

1. 基本原则
  • 关注最高频操作:复杂度主要由循环、递归等高频执行的语句决定。
  • 忽略常数、低阶项:分析时只保留增长速度最快的部分。
2. 常见法则
  1. 单段代码取高频
    • 例如循环,复杂度由循环次数决定。
    • for (int i = 0; i < n; i++) → O(n)
  1. 多段代码取最大(加法法则)
    • 如果一段代码包含多种复杂度,最终复杂度取其中量级最大的。
    • O(n) + O(n²) → O(n²)
  1. 嵌套代码取乘积(乘法法则)
    • 循环/递归嵌套时,复杂度是内外层复杂度的乘积。
    • for (i=0; i<n; i++) for (j=0; j<n; j++) → O(n²)
  1. 多个规模取加法
    • 当多个循环由不同变量控制时,复杂度相加。
    • for (i=0; i<n; i++) + for (j=0; j<m; j++) → O(n + m)

单段看循环,多段取最大,嵌套算乘积,不同规模相加。


几种常见时间复杂度实例分析

1. 多项式阶(常见、可接受)

  • O(1):常数阶,不随数据规模变化。
  • O(log n):对数阶,常见于二分查找。
  • O(n):线性阶,最常见的复杂度。
  • O(n log n):线性对数阶,常见于高效排序算法(归并、快排)。
  • O(n²)、O(n³):平方、立方阶,常见于多重循环。

2. 非多项式阶(性能极差)

  • O(2ⁿ):指数阶,常见于递归穷举。
  • O(n!):阶乘阶,常见于全排列问题。

时间复杂度示例

O(1)

执行次数与 n 无关:

int a = 1, b = 2;
int c = a + b;   // O(1)
O(log n)

指数增长的循环:

int i = 1;
while (i <= n) {
    i = i * 2;   // log₂n 次
}

→ 复杂度 O(log n)

O(n log n)

外层 O(n),内层 O(log n):

for (int i = 0; i < n; i++) {
    int j = 1;
    while (j <= n) {
        j = j * 2;
    }
}

→ 复杂度 O(n log n)

O(m + n)

两个规模独立的循环:

int cal(int m, int n) {
    int sum1 = 0, sum2 = 0;
    for (int i = 1; i < m; i++) sum1 += i;   // O(m)
    for (int j = 1; j < n; j++) sum2 += j;   // O(n)
    return sum1 + sum2;
}

→ 复杂度 O(m + n)
⚠️ 这里不能简单省略,因为 m 和 n 规模不确定。

O(m * n)

双重嵌套:

for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        // O(1)
    }
}

→ 复杂度 O(m * n)


空间复杂度分析

空间复杂度表示算法运行过程中额外需要的存储空间与数据规模 n 的关系。

示例:

void print(int n) {
    int i = 0;          // O(1)
    int[] a = new int[n];  // O(n)
    for (i = 0; i < n; i++) {
        a[i] = i * i;
    }
    for (i = n - 1; i >= 0; i--) {
        System.out.println(a[i]);
    }
}
  • int i:常量空间 O(1)
  • int[] a:需要 n 个空间 O(n)
  • 其余语句不占用额外空间

→ 总空间复杂度:O(n)

常见空间复杂度
  • O(1):常量空间(变量、计数器)。
  • O(n):线性空间(数组、链表)。
  • O(n²):二维数组。
  • (对数阶空间 O(log n)、O(n log n) 实际应用中很少见)。

总结:关注额外数据结构大小,常见 O(1)、O(n)、O(n²)。

时间复杂度 & 空间复杂度对照表

复杂度

时间复杂度示例代码

场景

空间复杂度示例代码

场景

O(1)

java int a = 1; int b = 2; int c = a + b;

常量时间,不随 n 增长

java int x = 10; // 常量变量

只使用常量存储

O(log n)

java int i = 1; while (i <= n) { i *= 2; }

二分查找、平衡树操作

少见,一般递归调用栈会近似 O(log n),如二分查找

递归深度 log n

O(n)

java for (int i = 0; i < n; i++) {}

遍历数组/链表

java int[] arr = new int[n];

存储一维数组

O(n log n)

java for (int i = 0; i < n; i++) { int j = 1; while (j <= n) { j *= 2; } }

高效排序(快排、归并排序)

少见,如分治需要额外数组

归并排序辅助数组

O(n²)

java for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) {}

嵌套循环(冒泡、选择、插入排序)

java int[][] matrix = new int[n][n];

存储二维矩阵

O(n³)

java for (int i=0;i<n;i++) for (int j=0;j<n;j++) for (int k=0;k<n;k++) {}

三重嵌套循环

很少见,三维数组存储

三维表格/立体数据

O(m+n)

java for (int i=0;i<m;i++) {} for (int j=0;j<n;j++) {}

两个规模独立循环

java int[] a = new int[m]; int[] b = new int[n];

同时存储 m 和 n

O(m*n)

java for (int i=0;i<m;i++) for (int j=0;j<n;j++) {}

双重循环

java int[][] grid = new int[m][n];

m×n 矩阵

O(2ⁿ)

java void dfs(int n) { if (n==0) return; dfs(n-1); dfs(n-1); }

递归穷举(子集、背包)

java // 一般仍为 O(n),存递归路径

回溯

O(n!)

java void permute(int[] arr, int l, int r) { if (l==r) return; for (int i=l;i<=r;i++){ swap(arr,l,i); permute(arr,l+1,r); swap(arr,l,i);} }

全排列问题

java // 一般仍为 O(n),存路径+标记数组

全排列存储



复杂度增长趋势

复杂度分析的四个概念

  1. 最坏时间复杂度:代码在最糟糕情况下的复杂度。
  2. 最好时间复杂度:代码在最理想情况下的复杂度。
  3. 平均时间复杂度:所有情况加权平均后的复杂度。
  4. 均摊时间复杂度:大多数操作是低复杂度,少数高复杂度操作有时序规律,可均摊为低复杂度。

常见数据结构

1、 线性表

线性表:   线性表就是数据排成像一条线一样的结构,每个线性表上的数据最多只有前和后两个方向,常见的线性表结构:数组,链表、队列、栈等。

数组

数组(Array)是一种线性表数据结构,使用一块连续的内存空间存储一组相同类型的数据。

特点:
  1. 连续的内存空间:数组需要一块连续的内存空间来存储数据。
  2. 相同类型的数据:数组中的所有元素必须是相同的数据类型。
  3. 优点:支持随机访问,访问效率高。
  4. 缺点:插入和删除操作效率较低,对内存空间要求较高。
随机访问的原理:

数组可以通过下标快速访问元素,其寻址公式为:

a[i]_address = base_address + i * data_type_size
  • base_address:数组首元素的内存地址。
  • i:数组下标。
  • data_type_size:数组中每个元素的大小。
插入和删除操作效率低的原因:
  • 插入:在数组的第 k 个位置插入数据时,需要将 k 到 n 的元素依次向后移动,最坏情况下时间复杂度为 O(n),最好情况下为 O(1)。
  • 删除:删除某个元素后,需要将其后续元素依次向前移动,时间复杂度同样为 O(n)。

最好时间复杂度O(1)

最坏情况复杂度为O(n)

数组插入和删除的复杂度分析
插入操作:
  1. 复杂度:
    • 最好情况:O(1)(直接在数组末尾插入,无需移动元素)。
    • 最坏情况:O(n)(在数组开头插入,需要移动所有元素)。
    • 平均情况:O(n)。
  1. 优化方式(无序数组):
    • 如果数组无序,可以将第 k 个位置的元素移动到数组末尾,然后将新元素插入到第 k 个位置。
    • 此时插入操作的复杂度为 O(1)。
删除操作:
  1. 复杂度:
    • 最好情况:O(1)(删除末尾元素,无需移动其他元素)。
    • 最坏情况:O(n)(删除开头元素,需要移动所有后续元素)。
    • 平均情况:O(n)。
  1. 优化方式(批量删除):
    • 记录已删除的数据,每次删除操作仅标记数据为“已删除”,而不立即移动数据。
    • 当数组存储空间不足时,触发一次真正的删除操作,清理标记为“已删除”的数据。
    • 这种方式类似于 JVM 的标记-清除垃圾回收算法,能够提高多次删除操作的效率。

链表(Linked List)

  1. 定义:链表是一种线性表数据结构,与数组类似,但其内存分布不要求连续。
  2. 内存结构:链表使用一组不连续的内存块,通过指针将这些内存块串联起来。
  3. 节点(Node):
    • 每个节点包含两部分:
      • 数据域:存储数据。
      • 指针域:存储指向下一个节点的地址(后继指针 next)。
  1. 特点:
    • 插入和删除操作效率高,不需要移动其他元素。
    • 随机访问效率低,需要从头节点开始遍历。

内存消耗:与数组相比,链表的内存消耗更大,因为每个节点除了存储数据外,还需要额外的空间来存储后继指针。

操作效率:

  • 插入、删除:效率较高,时间复杂度为 O(1),只需修改指针指向即可完成操作。
  • 随机访问:效率较低,时间复杂度为 O(n),需要从链表头部开始逐个遍历,直到找到目标节点。

单链表

  1. 节点结构:每个节点仅包含一个后继指针,用于指向链表中的下一个节点。
  2. 特殊节点:
    • 首节点:首节点的地址表示整条链表的起点。
    • 尾节点:尾节点的后继指针指向空地址(null),表示链表的终点。
  1. 性能特点:
    • 插入和删除:时间复杂度为 O(1),只需修改指针指向即可完成操作。
    • 查找:时间复杂度为 O(n),需要从首节点开始逐个遍历,直到找到目标节点。

循环链表

  1. 循环链表:与单链表的区别在于尾节点的后继指针指向首节点的地址,从而形成一个环状结构。
  2. 应用场景:适合处理具有循环特性的问题,例如约瑟夫问题、循环队列等。

双向链表

  1. 节点结构:双向链表的每个节点包含三个部分:数据域、前驱指针(prev)和后继指针(next)。 
    • 前驱指针指向前一个节点的地址。
    • 后继指针指向下一个节点的地址。
    • 首节点的前驱指针和尾节点的后继指针均指向空地址(null)。
  1. 性能特点: 
    • 存储空间:相比单链表,双向链表需要额外的存储空间来存储前驱指针。 
    • 插入和删除: 
      • 给定数据值删除节点:时间复杂度为 O(n),需要遍历链表找到目标节点。 
      • 给定节点地址删除节点:时间复杂度为 O(1),直接通过前驱指针找到前驱节点并修改指针即可。
    • 查询效率: 
      • 对于有序链表,双向链表可以通过记录上次查找的位置,根据大小关系决定查找方向,从而提高查询效率,平均只需查找一半的数据。

4.双向循环链表:

首节点的前驱指针指向尾节点,尾节点的后继指针指向首节点。


链表于数组比较

1.插入、删除和随机访问的时间复杂度
数组:插入、删除的时间复杂度是O(n),随机访问的时间复杂度是O(1)。
链表:插入、删除的时间复杂度是O(1),随机访问的时间复杂端是O(n)。

  1. 内存分配:
    • 数组:静态分配内存,内存连续,元素存储在栈区。
    • 链表:动态分配内存,内存不连续,元素存储在堆区。
  1. 长度:
    • 数组:长度固定,无法动态扩展,数据增加可能超出容量,减少会浪费内存。
    • 链表:动态分配内存,适应数据动态增减。
  1. 访问效率:
    • 数组:支持随机访问,时间复杂度为 O(1)。
    • 链表:不支持随机访问,时间复杂度为 O(n)。
  1. 插入和删除效率:
    • 数组:插入、删除需要移动元素,时间复杂度为 O(n)。
    • 链表:插入、删除只需修改指针,时间复杂度为 O(1)。
  1. CPU缓存友好性:
    • 数组:内存连续,CPU缓存友好,访问效率高。
    • 链表:内存不连续,CPU缓存不友好。

  • 可能导致堆内存溢出,因为无法找到足够大的连续内存空间。
  • 频繁的内存申请和释放会导致内存碎片,在 Java 中可能引发频繁的GC(垃圾回收)。

选择建议

  • 优先选择数组:如果数据量固定、需要高效随机访问、对内存使用不苛刻。
  • 选择链表:如果数据量动态变化、需要频繁插入和删除操作。

队列

队列是一种先进先出(FIFO, First In First Out)的线性表数据结构,支持以下两种基本操作:

特点:

  1. 抽象数据结构:队列是一种逻辑结构,具体实现可以基于数组或链表。
  2. 操作受限:
    • 入队(enqueue):在队尾插入元素。
    • 出队(dequeue):从队头删除元素。
  1. 先进先出:最早进入队列的元素最先被处理。

实现方式:

  1. 顺序队列(基于数组实现):
    • 优点:实现简单,访问效率高。
    • 缺点:数组长度固定,可能导致内存浪费或需要扩容。
  1. 链式队列(基于链表实现):
    • 优点:动态分配内存,适合数据动态变化的场景。
    • 缺点:需要额外的存储空间存储指针。

实现队列

队列可以用数组来实现,也可以用链表来实现。

用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。

基于数组的队列:

实现思路:

实现队列需要两个指针:一个是head指针,指向队头;一个是tail指针,指向队尾。你可以结合下面这幅图来理解。当a,b,c,d依次入队之后,队列中的head指针指向下标为0的位置, tail指针指向下标为4的位置。

当我们调用两次出队操作之后,队列中head指针指向下标为2的位置, tail指针仍然指向下标为4的位置.

随着不停地进行入队、出队操作, head和tail都会持续往后移动。当tail移 . ,动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。这个问题该如何解决呢?

在出队时可以不用搬移数据。如果没有空闲空间了,我们只需要在入队时,再集中触 ,发一次数据的搬移操作。

当队列的tail指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head到tail之间的数据,整体搬移到数组中0到tail-head的位置。

public class ArrayQueue {
    private int[] data; // 存储队列数据的数组
    private int head;   // 指向队头的指针
    private int tail;   // 指向队尾的指针
    private int size;   // 队列的当前大小

    public ArrayQueue(int capacity) {
        data = new int[capacity];
        head = 0;
        tail = 0;
        size = 0;
    }

    // 入队操作
    public boolean enqueue(int value) {
        if (size == data.length) {
            // 如果队列已满,触发数据搬移操作
            if (head == 0) {
                return false; // 队列已满且无法搬移
            }
            // 搬移数据
            for (int i = head; i < tail; i++) {
                data[i - head] = data[i];
            }
            tail -= head; // 更新tail指针
            head = 0;     // 重置head指针
        }
        // 插入新数据
        data[tail] = value;
        tail++;
        size++;
        return true;
    }

    // 出队操作
    public Integer dequeue() {
        if (size == 0) {
            return null; // 队列为空
        }
        int value = data[head];
        head++;
        size--;
        return value;
    }

    // 获取队列当前大小
    public int getSize() {
        return size;
    }

    // 判断队列是否为空
    public boolean isEmpty() {
        return size == 0;
    }
}

基于链表的实现: 

需要两个指针: head指针和tail指针,它们分别指向链表的第一个结,点和最后一个结点。

如图所示,入队时, tail->next= new node, tail = tail->next:出队时, head = head->next.

public class LinkedQueue {
    private static class Node {
        int value;
        Node next;

        Node(int value) {
            this.value = value;
            this.next = null;
        }
    }

    private Node head; // 指向队头
    private Node tail; // 指向队尾

    public LinkedQueue() {
        head = null;
        tail = null;
    }

    // 入队操作
    public void enqueue(int value) {
        Node newNode = new Node(value);
        if (tail != null) {
            tail.next = newNode; // 将新节点连接到队尾
        }
        tail = newNode; // 更新队尾指针
        if (head == null) {
            head = tail; // 如果队列为空,更新队头指针
        }
    }

    // 出队操作
    public Integer dequeue() {
        if (head == null) {
            return null; // 队列为空
        }
        int value = head.value;
        head = head.next; // 更新队头指针
        if (head == null) {
            tail = null; // 如果队列为空,重置队尾指针
        }
        return value;
    }

    // 判断队列是否为空
    public boolean isEmpty() {
        return head == null;
    }
}

循环队列

我们刚才用数组来实现队列的时候,在tail==n时,会有数据搬移操作,这样入队操作性能就会受到影响。那有没有办法能够避免数据搬移呢?我们来看看循环队列的解决思路。循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相,连,板成了一个环。我画了一张图,你可以直观地感受一下。

我们可以看到,图中这个队列的大小为8,当前head-4, tail-7,当有一个新的元素a入队时, .我们放入下标为7的位置。但这个时候,我们并不把tail更新为8,而是将其在环中后移一位,到下标为0的位置。当再有一个元素b入队时,我们将b放入下标为0的位置,然后tail加1更新为1,所以,在a, b依次入队之后,循环队列中的元素就变成了下面的样子:

队列为空的判断条件是head == tail,但队列满的判断条件就稍微有点复杂了。我画了一张队列满的图,你可以看一下,试着总结一下规律,

就像我图中画的队满的情况, tail=3, head-4, n=8,所以总结一下规律就是: (3+1)%8-4,多画几张队满的图,你就会发现,当队满时, (tail+1)%n=head..你有没有发现,当队列满时,图中的tail指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

解决浪费一个存储空间的思路:定义一个记录队列大小的值size,当这个值与数组大小相等时,表示队列已满,当tail达到最底时,size不等于数组大小时,tail就指向数组第一个位置。当出队时,size—,入队时size++

阻塞队列

阻塞队列和并发队列(应用比较广泛)

阻塞队列其实就是在队列基础上增加了阻塞操作。

简单来说,就是在队列为空的时候,从队头取数 , 据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

你应该已经发现了,上述的定义就是一个"生产者-消费者模型" !是的,我们可以使用阻塞队列,轻松实现一个"生产者-消费者模型" !这种基于阻塞队列实现的"生产者-消费者模型" ,可以有效地协调生产和消费的速度。当"生产 , 者"生产数据的速度过快, "消费者"来不及消费时,存储数据的队列很快就会满了。这个时候,生产者就阻塞等待,直到"消费者"消费了数据, "生产者"才会被唤醒继续"生产而且不仅如此,基于阻塞队列,我们还可以通过协调"生产者"和"消费者"的个数,来提高数据,的处理效率。比如前面的例子,我们可以多配置几个"消费者" ,来应对一个"生产者"


小结

队列最大的特点就是先进先出,主要的两个操作是入队和出队。

它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。

长在数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,我们就,需要像环一样的循环队列。要想写出没有bug的循环队列实现代码,关键要确定好队空和队满的,判定条件。

阻塞队列、并发队列,底层都还是队列这种数据结构,只不过在之上附加了很多其他功能。阻塞队列就是入队、出队操作可以阻塞并发队列就是队列的操作多线程安全。

 4、散列表

什么是散列表:

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

原理:

散列表用的就是数组支持按照下标随机访问的时候,时间复杂度是0(1)的特性。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组标标,从对应的数组下标的位置取数据。

散列函数的设计要求:

  1. 散列函数计算得到的散列值是一个非负整数;.
  2. 如果key1 = key2,那hash(key1) == hash(key2);
  3. 如果key1 != key2,那hash(key1)  !=  hash(key2),

散列函数的设计不能太复杂,散列函数生成值要尽可能随机并且均匀分布

如果不符合3 那么就出现了散列冲突,散列冲突是无法避免的

解决散列冲突的方法有两种: 

开放寻址法(open addressing)和链表法(chaining)

开放寻址法:如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。

装在因子:  散列表中一定比例的空闲槽位。公式: 散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

链表法:

链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。我们来看这个图,在散列表中,每个"桶(bucket) "或者"槽(slot) "会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

常见算法

1、递归算法

一、什么是递归?

1.递归是一种非常高效、简洁的编码技巧,一种应用非常广泛的算法,比如DFS深度优先搜索、前中后序二叉树遍历等都是使用递归。
2.方法或函数调用自身的方式称为递归调用,调用称为递,返回称为归。
3.基本上,所有的递归问题都可以用递推公式来表示,比如
f(n) = f(n-1) + 1; 
f(n) = f(n-1) + f(n-2);
f(n)=n*f(n-1);

二、为什么使用递归?递归的优缺点?

1.优点:代码的表达力很强,写起来简洁。
2.缺点:空间复杂度高、有堆栈溢出风险、存在重复计算、过多的函数调用会耗时较多等问题。

三、什么样的问题可以用递归解决呢?

一个问题只要同时满足以下3个条件,就可以用递归来解决:
1.问题的解可以分解为几个子问题的解。何为子问题?就是数据规模更小的问题。
2.问题与子问题,除了数据规模不同,求解思路完全一样
3.存在递归终止条件

四、如何实现递归?

1.递归代码编写
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
2.递归代码理解
对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维误区。
那该如何理解递归代码呢?如果一个问题A可以分解为若干个子问题B、C、D,你可以假设子问题B、C、D已经解决。而且,你只需要思考问题A与子问题B、C、D两层之间的关系即可,不需要一层层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。
因此,理解递归代码,就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

递归的关键是终止条件
五、递归常见问题及解决方案

1.警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。
2.警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。

六、如何将递归改写为非递归代码?

笼统的讲,所有的递归代码都可以改写为迭代循环的非递归写法。如何做?抽象出递推公式、初始值和边界条件,然后用迭代循环实现。

2、排序



一、排序方法与复杂度归类
(1)几种最经典、最常用的排序方法:冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。
(2)复杂度归类
冒泡排序、插入排序、选择排序 O(n^2)
快速排序、归并排序 O(nlogn)
计数排序、基数排序、桶排序 O(n)

二、如何分析一个“排序算法”?
<1>算法的执行效率
1. 最好、最坏、平均情况时间复杂度。
2. 时间复杂度的系数、常数和低阶。
3. 比较次数,交换(或移动)次数。
<2>排序算法的稳定性
1. 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
2. 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
3. 举例:给电商交易系统中的“订单”排序,按照金额大小对订单数据排序,对于相同金额的订单以下单时间早晚排序。用稳定排序算法可简洁地解决。先按照下单时间给订单排序,排序完成后用稳定排序算法按照订单金额重新排序。
<3>排序算法的内存损耗
原地排序算法:特指空间复杂度是O(1)的排序算法。

常见的排序算法:


冒泡排序


冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不满足就让它俩互换。

代码:

  public int[] bubbleSort(int[] a) {
        int n = a.length;
        if (n<=1) {
            return a;
        }
        for (int i = 0; i < n; i++) {
            //提前退出冒泡循环的标志
            boolean flag = false;
            for (int j = 0; j < n-i-1; j++) {
                if (a[j]>a[j+1]) {//
                    int temp = a[j];
                    a[j] = a[j+1];
                    a[j+1] = temp;

                    flag = true;//表示有数据交换
                }
                if (!flag) {
                    break; //没有数据交换(说明已排好序无需再进行冒泡),提前退出
                }
            }
        }
        return a;
    }


四、插入排序


插入排序将数组数据分成已排序区间和未排序区间。初始已排序区间只有一个元素,即数组第一个元素。在未排序区间取出一个元素插入到已排序区间的合适位置,直到未排序区间为空。

代码:

    public int[] insertionSort(int[] a) {
		int n = a.length;
		if (n<=1) return a;
		
		for (int i = 1; i < n; i++) {
			int value = a[i];
			int j = i-1;
			for (; j >=0; j--) {
				if (a[j] > value) {
					a[j+1] = a[j];//移动数据
				}else {
					break;
				}
			}
			a[j+1] = value;//插入数据
		}
		
		return a;
	}


五、选择排序


选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入已排序区间的末尾,直到未排序区间为空。
代码:

public int[] selectionSort(int[] a) {
		int n = a.length;
		
		for (int i = 0; i < a.length - 1; i++) {
			for (int j = i+1; j < a.length; j++) {
				//交换
				if (a[i] > a[j]) {
					int temp = a[i];
					a[i] = a[j];
					a[j] = temp;
				}
			}
		}
		
		return a;
	}

六、归并排序

如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

 实现思路:

merge-sort(p...r)表示,给下标从p到r之间的数组排序。我们将这个排序问题转化为了两个子问 ,题, merge_sort(p...q)和merge-sort(q+1..r),其中下标q等于p和r的中间位置,也就是, (p+r)/2,当下标从p到q和从q+1到r这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从p到r之间的数据就也排好序了。

代码:

 // 归并排序算法, a是数组,n表示数组大小
  public static void mergeSort(int[] a, int n) {
    mergeSortInternally(a, 0, n-1);
  }

  // 递归调用函数
  private static void mergeSortInternally(int[] a, int p, int r) {
    // 递归终止条件
    if (p >= r) return;

    // 取p到r之间的中间位置q
    int q = (p+r)/2;
    // 分治递归
    mergeSortInternally(a, p, q);
    mergeSortInternally(a, q+1, r);

    // 将A[p...q]和A[q+1...r]合并为A[p...r]
    merge(a, p, q, r);
  }

  private static void merge(int[] a, int p, int q, int r) {
    int i = p;
    int j = q+1;
    int k = 0; // 初始化变量i, j, k
    int[] tmp = new int[r-p+1]; // 申请一个大小跟a[p...r]一样的临时数组
   
    // 1 排序
    while (i<=q && j<=r) {
      if (a[i] <= a[j]) {
        tmp[k++] = a[i++]; // i++等于i:=i+1
      } else {
        tmp[k++] = a[j++];
      }
    }

    // 2 判断哪个子数组中有剩余的数据
    int start = i;
    int end = q;
    if (j <= r) {
      start = j;
      end = r;
    }

    // 3 将剩余的数据拷贝到临时数组tmp
    while (start <= end) {
      tmp[k++] = a[start++];
    }

    // 4 将tmp中的数组拷贝回a[p...r]
    for (i = 0; i <= r-p; ++i) {
      a[p+i] = tmp[i];
    }
  }

merge是这样执行的:

代码分析:

七、快速排序

快排的思想:    如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot (分区点) 。我们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。

快排利用的分而治之的思想

八、线性排序:

时间复杂度O(n)

我们把时间复杂度是线性的排序算法叫作线性排序(Linear sort)常见的线性算法有: 桶排序、计数排序、基数排序

特点:

非基于比较的排序算法 

桶排序

桶排序,顾名思义,会用到“桶" ,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

对排序的数据要求苛刻:

1, 要排序的数据需要很容易就能划分成m个桶,并且,桶与桶之间有着天然的大小顺序。

2 ,数据在各个桶之间的分布是比较均匀的。

3 ,桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

计数排序

计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。

计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

代码:

 // 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数。
  public static void countingSort(int[] a) {
	int n = a.length;
    if (n <= 1) return;

    // 查找数组中数据的范围
    int max = a[0];
    for (int i = 1; i < n; ++i) {
      if (max < a[i]) {
        max = a[i];
      }
    }

    // 申请一个计数数组c,下标大小[0,max]
    int[] c = new int[max + 1];
    for (int i = 0; i < max + 1; ++i) {
      c[i] = 0;
    }

    // 计算每个元素的个数,放入c中
    for (int i = 0; i < n; ++i) {
      c[a[i]]++;
    }

    // 依次累加
    for (int i = 1; i < max + 1; ++i) {
      c[i] = c[i-1] + c[i];
    }

    // 临时数组r,存储排序之后的结果
    int[] r = new int[n];
    // 计算排序的关键步骤了,有点难理解
    for (int i = n - 1; i >= 0; --i) {
      int index = c[a[i]]-1;
      r[index] = a[i];
      c[a[i]]--;
    }

    // 将结果拷贝会a数组
    for (int i = 0; i < n; ++i) {
      a[i] = r[i];
    }
  }

数据结构与算法(python)(数据结构) 整理了数据结构的基础内容,包括列表、栈和队列、链表、哈希表和树的相关知识点 阅读详情

相关推荐

【数据结构与算法】常见数据结构及基本操作

数据结构及基本操作+排序算法+查找算法目录1.数据结构与算法常见概念2.数据逻辑结构2.1线性结构2.2树形结构2.3图形结构2.4集合结构3.排序算法冒泡排序简单选择排序直接插入排序希尔排序堆排序归并排序快速排序4.查找算法顺序表查找有序表查找线性索引查找二叉排序树平衡二叉树(AVL树)多路查找树(B树)散列表查找(哈希表) 总结《大话数据结构》,文后附《大话数据结构》下载链接,本文相关代码均由...

程序媛养成记 9万+

数据结构与算法

数据结构与算法 1.数据结构的概念 数据结构指的是一组数据的存储结构。 2.算法的概念 算法是指操作数据的一组方法 3.二者的关系 数据结构是为算法服务的,而算法要作用在特定的数据结构上。 4.最常用的数据结构预算法 数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Tire树 算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。 一、算法的复杂度 1.1大O复杂度表示法: T(n)=O(f(n)). 其中T(n)表示代码执行的时间;n表示数据规

m0_52397715的博客 1万+

基于SIMULINK的六足机器人仿真

六足机器人是机电高度集成的仿生系统,它的动态性能由其结构系统与控制系统一起决定。为了提高六足机器人整体的动态性能,对六足机器人进行集成优化设计。描述六足机器人系统的结构;根据六足机器人机构的几何特征,讨论在优化过程中涉及到的设计变量、约束方程以及目标函数;对六足机器人系统进行集成优化建模。 六足仿生机器人是指模仿六足生物的身体结构、运动形式以及功能特征的机器人。这种机器人同时具有足式和仿生机器人的优点,具有良好的运动控制、位姿调整以及信息融合等能力。此外,六足机器人具有丰富的...

FPGA/MATLAB学习教程/源码/项目合作开发 1万+

数据结构和算法

java数据结构和算法

hxs214的博客 5349

【AI Infra】Perfetto性能分析工具指南

Perfetto是一款开源性能分析工具,提供系统级和应用级的活动数据追踪功能。其界面分为四个区域:左侧操作区(包含Trace查看、SQL查询、预置分析等功能)、上方信息区、中间图形化展示区以及下方详细信息区。工具支持快捷键操作(w/s缩放、a/d移动、m标记等),并能高效处理大体积Trace文件。通过Timeline分析可发现性能瓶颈,如案例中update_actor耗时过高的问题,最终定位到CPU offloading导致的MemcpyH2D操作耗时过长,提出了优化方案。Perfetto相比Systrac

baiweizeng的博客 1509

FEniCS学习笔记01:centos7下通过conda安装FEniCSx0.7

2023年版本的Fenicsx0.7,官网推荐使用conda命令安装集成好的Python包,因此我们的安装思路是先安装Anaconda,再按照官网步骤安装Fenics。

huahua_xiao的博客 5057

高级数据结构与算法学习笔记

A: 快速排序的平均时间复杂度是O(nlogn),而冒泡排序是O(n²)。快速排序通过分治策略,每次将数组分成两部分,大大减少了比较次数。A: 哈希表通过哈希函数直接定位到存储位置,时间复杂度为O(1)。它避免了遍历查找的过程,是一种空间换时间的策略。A: 只有数据有序,才能通过比较中间元素来缩小查找范围。如果数据无序,就无法保证目标值一定在某个子区间内。Q: 为什么快速排序比冒泡排序快?Q: 二分查找为什么要求数据有序?Q: 哈希表为什么查找效率高?Q: 如何选择排序算法?

weixin_74748597的博客 962

踩坑记录:服务商模式下微信小程序支付,为什么必须用子商户的AppID获取openid?

本文深入解析了服务商模式下微信小程序支付必须使用子商户AppID获取openid的技术原因,详细对比了服务商模式与直连模式的关键差异,并提供了正确的配置指南和常见问题解决方案,帮助开发者避免微信支付集成中的常见陷阱。

weixin_28318011的博客 550

数据结构与算法学习笔记结构图

本文是王争老师的《算法与数据结构之美》的学习笔记,详细内容请看王争的专栏 。有不懂的地方指出来,我做修改。 数据结构与算法思维导图 数据结构指的是“一组数据的存储结构”,算法指的是“操作数据的一组方法”。 数据结构是为算法服务的,算法是要作用再特定的数据结构上的。 最常用的数据结构预算法: 数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Tire树 算法: 递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。 王卓老师:一个接一个的.

qq_64594090的博客 381

MinIO vs 华为云OBS:成本、性能与兼容性深度对比(含实测数据)

本文深度对比了开源对象存储MinIO与华为云OBS在成本、性能、兼容性及运维方面的核心差异。通过实测数据与全生命周期成本分析,为技术决策者提供选型依据。文章指出,MinIO在长期成本控制、性能深度优化及S3 API兼容性方面优势显著,而华为云OBS则提供开箱即用的便利性。

kmeans3miner的博客 593

DeepSeek与豆包无缝对接:5分钟搞定智能周报自动化(附完整代码)

本文详细介绍了如何利用DeepSeek与豆包API,快速搭建一个智能周报自动化系统。通过整合豆包平台的工作流数据,并调用DeepSeek大模型进行深度分析与内容生成,该系统能在5分钟内自动抓取、分析多源信息,并生成结构清晰、富有洞察力的团队周报,显著提升工作效率。

weixin_29029995的博客 493

《数据结构与算法之美》学习笔记一

所以说就像这门课程的作者王争老师提到的,不管是数据结构与算法,还是其他的原理性的知识、提高性的知识,你不学,就永远觉得没用,但是你学了,总有一天你会应用上,并且比普通的笨方法更优雅。对一组数据结构进行连续操作时,大部分情况下时间复杂度都很低,只有个别情况时间复杂度比较高,并且在高时间复杂度后面,跟着连续的低时间复杂度的操作,此时就可以将一组操作放到一块儿进行分析,将高时间复杂度平均到低时间复杂度上,能够使用均摊分析的场景中,一般均摊时间复杂度就等于最好情况时间复杂度。但是我们可以知道相对的执行时间。

weixin_45855469的博客 1329

VMware Workstation Pro 16 安装 Ubuntu 20.04

VMware Workstation Pro 16 安装 ubuntu 20.04目录环境VMware Workstation下载Ubuntu 20.04下载安装过程 目录 环境 Windows 10 Workstation 16 Pro for Windows Ubuntu 20.04 LTS 64位 desktop版本 VMware Workstation下载 链接: https://www.vmware.com/products/workstation-pro/workstation-pro-eval

qq_45642410的博客 1万+

数据结构与算法学习笔记----单调栈

@ author: 明月清了个风。

weixin_60278491的博客 1062

javascript 酷炫时钟

javascript 酷炫时钟

liuhao9999的博客 631

上海计算机学会2020年5月月赛C++乙组T2经济普查

中国正在以前所未有的力度推进脱贫攻坚,国家计划在2020年使得在现行标准下的贫困人口实现全面脱贫,为此,需要对居民的经济状况进行普查。 题目描述 给定一个正整数n,表示人口数量。一开始,每个居民拥有的财产记为a1​,a2​,⋯,an​,财产可能发生两种变化: 一种来源于个体,由某个居民的经济活动而产生。在某个时刻,某个居民的财产会直接变成某个数值,可能升高也可能降低。 一种来源于政策,在某个时刻,国家会对财产低于某个标准线全部的居民,实施政策支持,使得他们的财产全部正好到达标准线。 给定这些变

长春高老师编程 908

数据结构与算法(Python)学习笔记PDF

数据结构与算法(Python)学习笔记PDF 去发现同类优质开源项目:https://gitcode.com/ 欢迎来到数据结构与算法的Python之旅!这份精心整理的数据结构与算法(Python).pdf是专为Python编程爱好者和想要深入理解数据结构及算法的朋友们准备的宝贵资料。通过这份笔记,你将能够系统地学习到如何在Python语言中实现和运用各种基础及高级的数据结构,以及相应的算法策略。...

gitblog_06623的博客 943

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

【数据结构与算法学习笔记】

本系列文章为作者学习数据结构与算法所作笔记,笔记参考陈斌老师的课程“数据结构与算法 Python版”和经典的数据结构教材《数据结构(C语言版)》(作者:严蔚敏),后期也参考了LetCode的题解。

floating_heart的博客 3101

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

杭电 数据结构与算法分析 学习笔记

别在外面窥探 进来

JJjiangfz的博客 470

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

数据结构与算法基础学习笔记--第一周

从具体问题中抽象出来的一个数学模型以及定义在此数学模型上的一组操作的总和。可以用(D,S,P)三元组表示。D是数据对象,S是D上的关系集,P是对D的基本操作集。

qq_56740360的博客 793

[算法学习笔记] 《Hello算法》第3章 数据结构

本系列为《Hello算法》学习笔记,仅供学习,不做商用,如有侵权,联系我删除即可!常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。逻辑结构揭示了数据元素之间的逻辑关系:如下图所示,逻辑结构可分为“线性”和“非线性”两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。非线性数据结构可以进一步划分为树形结构和网状结构。当算法程序运行时,正在处理的数据主要存储在内存中。下图展示了一个计算机内存条,其中每个黑色方块

努力学习 972

【数据结构和算法-学习笔记】

【数据结构和算法-学习笔记】 01数据结构和算法绪论:    posted on 2017-05-11 08:56nodeyang 阅读(...) 评论(....

102

【数据结构与算法】数据结构-王道学习笔记(一)

线性表是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。typedef struct LNode{//定义单链表结点类型//数据域//指针域可以利用typedef关键字——数据类型重命名:type单链表的两种实现方式:1、不带头结点的单链表```bash//初始化一个空的单链表。

m0_58167298的博客 1060

小甲鱼数据结构&算法教程学习笔记01

小甲鱼数据结构&算法教程学习笔记 一、绪论 程序设计=数据结构+算法 数据结构:数据元素之间的一种或多种特定关系的集合 逻辑结构 数据对象中数据元素间的相互关系 集合结构 线性结构 树形结构 图形结构 物理结构 数据的逻辑结构在计算机中的存储形式 存储器主要针对内存而言,硬盘/软盘/光盘等数据组织多是文件形式。 数据元素存储结构 顺序存储 数据元素存放在地址连续的存储单元里,其数据间的逻辑关系与物理关系一致 链式存储 数据元素存储在任意的存储单元里,可以连续或不连

志威梦优快云博客 324
上一篇: Centos7搭建maven私服nexus3
下一篇: Jenkins使用笔记
刘彦青-Yannis
博客等级 码龄10年 1万+粉丝 234原创
评论 93
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

刘彦青-Yannis

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

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

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

打赏作者

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

抵扣说明:

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

余额充值