算法图解(3~4)

递归

在程序中递归指函数调用自己,递归只是让解决方案更清晰,并没有性能上的优势

如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解

每个递归函数都有两部分:

基线条件(base case):不再调用自己,从而避免形成无限循环。

比如被处理数据的最基础情况

递归条件(recursive case):函数调用自己,通常用状态转移方程

不断缩小问题规模

(Stack)

原理:遵循“后进先出”,你只能从顶部添加(推入)或移除(弹出)

优点:简单高效,时间复杂度低

缺点:可能导致栈溢出,访问不灵活

适用场景:

  • 函数调用和递归的实现
  • 回溯算法,DFS

调用栈:用于保存每个函数调用的状态信息,包括参数、局部变量、返回地址等。当函数调用时,当前的执行状态被保存到栈中,当函数执行结束时,该栈帧从栈中弹出,控制权返回到调用函数的上一个位置。

递归调用栈:每次调用自己,类似于不断压入调用栈的过程,直到达到基线条件,递归调用开始返回值,并逐步从调用栈中弹出。

而非递归形式:需要新的数据结构存储满足条件的,非栈的自动管理调用

欧几里得算法

也称为辗转相除法,是用于计算两个非负整数的最大公约数

如果 ab 是两个正整数,且 a > b,那么 ab 的最大公约数等于 ba % b 的最大公约数。

int gcd(int a, int b) {
    if (b == 0) {
        return a;
    }
    return gcd(b, a % b);
}

快速排序(排序算法)

原理:通常采用分治法策略。它通过选择一个基准pivot元素,将待排序数组分成较小元素和较大元素的两个子数组,然后递归地对这两个子数组进行排序。

优点:平均性能优越,只需要常数级别的额外空间,适合处理大规模数据。

缺点:最坏情况性能较差,不稳定排序(相等元素的相对顺序可能会发生改变)

时间:基准选择的好坏直接影响排序的效率,最好 == 平均O(nlogn),最坏O(n^2)

空间:O(logn)

实例:给定数组,并将所有小于基准元素(right)移到前面,并从所有<基准的元素最后位置划分为<部分和>部分,再将子数组递归排序

void quickSort(int arr[], int left, int right) {
    if (left < right) {
        int pivotIndex = partition(arr, left, right);

        quickSort(arr, left, pivotIndex - 1);

        quickSort(arr, pivotIndex + 1, right);
    }
}
int partition(int arr[], int left, int right) {
    int pivot = arr[right]; 
    int i = left - 1;
    
    for (int j = left; j < right; j++) {
        if (arr[j] < pivot) {
            i++;
            swap(arr[i], arr[j]); 
        }
    }
    swap(arr[i + 1], arr[right]);
    return i + 1; 
}

归并 | 合并 排序(排序算法)

原理:是一种经典的基于分治法的排序算法。它通过递归地将数组分成两半,分别排序,然后将两个有序的子数组合并成一个完整的有序数组

优点:是稳定的排序算法,元素在排序后仍然保持相对顺序不变,性能比较稳定。

缺点:空间复杂度高,不适合小规模数据排序

时间:O(nlogn)

空间:O(n)

实例:给定数组,选取中点分解为子数组,从调用栈顶部开始,依次合并merge

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        mergeSort(arr, left, mid);

        mergeSort(arr, mid + 1, right);

        merge(arr, left, mid, right);
    }
}

void merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    int leftArr[n1], rightArr[n2];

    for (int i = 0; i < n1; i++)
        leftArr[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        rightArr[j] = arr[mid + 1 + j];

    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (leftArr[i] <= rightArr[j]) {
            arr[k] = leftArr[i];
            i++;
        } else {
            arr[k] = rightArr[j];
            j++;
        }
        k++;
    }

    while (i < n1) {
        arr[k] = leftArr[i];
        i++;
        k++;
    }

    while (j < n2) {
        arr[k] = rightArr[j];
        j++;
        k++;
    }
}

快排 VS  归并

既然快排最坏时间慢于归并,为什么不用归并?

通常会忽略时间复杂度的常量,但有时候,常量的影响可能很大

快排的常量比合并要小,因此大部分情况(非最坏情况)都要优于合并的时间

快排何时慢?

比如一个有序数组,从左到右取基准点,这会导致调用栈高度为n

如果选择中间元素,调用栈高度为log^n,归并排序正是这样选择的

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Dijkstra算法是一种用于解决单源最短路径问题经典算法。它可以找到从一个顶点到其他所有顶点的最短路径。 下面是Dijkstra算法的图解步骤: 1. 创建一个距离数组dist[],用于存储从源顶点到其他顶点的最短距离。初始时,将源顶点的距离设置为0,其他顶点的距离设置为无穷大。 2. 创建一个集合visited[],用于记录已经找到最短路径的顶点。 3. 重复以下步骤,直到所有顶点都被访问: a. 从未访问的顶点中选择距离最小的顶点,将其标记为visited[]。 b. 更新与该顶点相邻的顶点的最短距离。如果通过该顶点到达相邻顶点的距离比当前记录的最短距离小,则更新最短距离。 4. 最终,dist[]数组中存储的就是从源顶点到其他所有顶点的最短距离。 下面是一个简单的图解示例: 假设有以下图形: ``` 2 (A)-->(B) | \ /| | \ / | 5| \ / |1 | \/ | | /\ | (C) / \ | | / \ | | / \| (D)------>(E) 3 ``` 我们以顶点A为源顶点,开始执行Dijkstra算法: 1. 初始化dist[]数组,将A的距离设置为0,其他顶点的距离设置为无穷大。 2. 选择距离最小的顶点A,并将其标记为visited[]。 3. 更新与A相邻的顶点的最短距离:B的距离更新为2,C的距离更新为5。 4. 继续选择距离最小的未访问顶点,此时是B。 5. 更新与B相邻的顶点的最短距离:C的距离更新为3,E的距离更新为3。 6. 继续选择距离最小的未访问顶点,此时是E。 7. 更新与E相邻的顶点的最短距离:D的距离更新为6。 8. 继续选择距离最小的未访问顶点,此时是C。 9. 更新与C相邻的顶点的最短距离:D的距离更新为5。 10. 继续选择距离最小的未访问顶点,此时是D。 11. 更新与D相邻的顶点的最短距离:E的距离更新为8。 12. 所有顶点都被访问,算法结束。 最终,dist[]数组中的值为: A: 0 B: 2 C: 3 D: 5 E: 3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值