首页 > 编程语言 >分治策略——算法学习(二)

分治策略——算法学习(二)

时间:2024-12-24 13:19:32浏览次数:8  
标签:策略 递归 int 分治 问题 算法 分解 排序 逆序

分治策略——算法学习(二)


前言

在算法设计的世界中,分治策略(Divide and Conquer)是一种极具魅力的思想,它通过将复杂问题拆解为多个规模较小但结构类似的子问题,从而以递归的方式解决问题。这种策略不仅高效而且直观,为许多经典算法的诞生奠定了基础,如快速排序(Quick Sort)、归并排序(Merge Sort)和二分查找(Binary Search)等。

(本文代码均使用c#语言)

概念

分治策略是一种经典的算法设计范式,核心思想是将一个复杂的问题分解为多个相互独立的较小子问题,分别解决这些子问题,然后将子问题的解合并起来得到原问题的解。

核心步骤

  1. 分解(Divide)
    将原问题分解为若干规模较小、性质与原问题相似的子问题。这些子问题通常是通过递归处理的

  2. 解决(Conquer)
    当子问题足够简单时,直接求解;否则,继续递归分解,直到子问题可以被直接解决。

  3. 合并(Combine)
    将子问题的解按一定规则合并成原问题的解。

适用情况

  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
  3. 利用该问题分解出的子问题的解可以合并为该问题的解
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题

归并排序

分解待排序的n个元素序列成各具n/2个元素的两个子序列,然后使用归并排序递归地排序两个子序列,最后合并两个已排序的子序列以产生已排序的答案(归并排序不是原址排序

归并排序伪代码:

//辅助过程
Function merge(A,p,q,r):
	n1 <- q - p + 1;
	n2 <- r - q;

	//分解
	//即A[p ,... ,q],R[q+1, ... ,r]
	let L[1 ... n1 + 1] and R[1 ... n2 + 1] be new arrays; 
	for i <- 1 to n1 do
		L[i] <- A[p + i - 1];
	end
	for j <- 1 to n1 do
		R[j] <- A[q+j];
	end
	L[n1 + 1] <- ∞; //哨兵
	R[n2 + 1] <- ∞; //哨兵
	i <- 1;
	j <- 1;

	//合并
	for k <- p to r do  //k所在的位置就是下一个数要插入的位置
		if L[i] <= R[j] then
			A[k] <- L[i];
			i <- i+1;
		end
		else
			A[k] <- R[j];
			j <- j+1;
		end
	end
end

//完整递归
Function merge-sort(A,p,r):
	if p < r then
		q <- [(p+ r) / 2];
		merge-sort(A,p,r); //递归分解左序列,直到只剩一个数
		merge-sort(A,q + 1,r); //递归分解右序列,直到只剩一个数
		merge(A,p,q,r); //分解完成后每层按照规则排序插入
	end
end

例题:求逆序对

求一组数的逆序对(给定一个数组 A[1…n]A[1 \dots n]A[1…n],如果数组中存在两个下标 i 和 j,满足: i<j,A[i]>A[j]那么,(i,j) 就被称为数组中的一个逆序对)

输入说明

第一行一个整数n(1≤n≤106),这组数中数字的个数。第二行n个整数,第i个整数ai(1≤ai≤106)表示第i个数(数字可能相同)

输出说明

一个整数,表示逆序对数量

输入样例

5
2 3 8 6 1

输出样例

5

解答

可以使用归并排序对数据进行排序,在合并过程中,原序列的逆序对数量等于两个子序列的逆序对数量加两个子序列之间的逆序对数量,而右边序列每个元素所贡献的逆序对数量等于左边序列中比它大的元素个数。所以子序列元素的顺序不会影响子序列间的逆序对数量,这样我们就可以在归并排序的过程中统计逆序对数量。

完整代码

using System;

class Test {
    static int InversionsMerge(int[] A, int p, int q, int r) {
        int n1 = q - p + 1;
        int n2 = r - q;
        int[] L = new int[n1 + 1];
        int[] R = new int[n2 + 1];
        int i = 0;
        int j = 0;
        for (i = 0; i < n1; i++) {
            L[i] = A[p + i];
        }
        for (j = 0; j < n2; j++) {
            R[j] = A[q + j + 1];
        }
        L[n1] = int.MaxValue;
        R[n2] = int.MaxValue;
        i = 0;
        j = 0;
        int inversions = 0; //定义变量用于统计逆序对
        for (int k = p; k <= r; ++k) {
            if (L[i] <= R[j]) {
                A[k] = L[i];
                ++i;
            }
            else {
                inversions += n1 - i; //逆序对数量等于左边序列比右边序列元素大的元素个数
                A[k] = R[j];
                ++j;
            }
        }
        return inversions;//返回逆序对数量
    }
    static int InversionsMergeSort(int[] A, int p, int r) {
        if (p >= r) return 0;
        int q = (p + r) / 2;
        int inversions = 0;
        inversions += InversionsMergeSort(A, p, q);//递归中累计逆序对
        inversions += InversionsMergeSort(A, q + 1, r);//递归中累计逆序对
        inversions += InversionsMerge(A, p, q, r);//递归中累计逆序对
        return inversions;//返回最终逆序对数量
    }
    static void Main() {
        int n = int.Parse(Console.ReadLine());
        int[] A = new int[n];
        string[] inputs = Console.ReadLine().Split(' ');
        for (int i = 0; i < n; i++) {
            A[i] = int.Parse(inputs[i]);
        }
        Console.WriteLine(InversionsMergeSort(A, 0, n - 1));
        Console.ReadLine();
    }
}

分析

  1. 解空间:
    归并排序算法的解空间可以视为一个递归分层的二叉树,每个节点表示一个子问题,这种树的深度为 O(log⁡n)O(\log n)O(logn),宽度在最底层达到最大,为 n 个叶子节点。
  2. 时间复杂度:
    为了简化分析,假设问题的规模n是2的幂。那么分解需要常量时间D(n)=\(\Theta\)(1); 递归地求解2个规模为n/2的问题需要2T(n/2)的时间; 最后在一个具有n个元素的子数组上运行merge的过程需要\(\Theta\)(n)的时间,所以D(n)=\(\Theta\)(n)。由于使用递归分解,分解过程相当于将问题规模逐步缩小一半,直到子数组的大小为 1,因此,递归分解的层数是 O(log⁡n)。综上,总时间复杂度为O(nlogn)

二分查找

找原序列的中间元素下标mid,将原序列分解为L=(a1,a2,...,amid)和R=(amid+1,amid+2,...,an)两个子序列。若v<=amid则递归地在序列L中查找v,否则递归地在序列R中查找v。若序列只有一个元素则检查唯一的序列元素是否和v相等(无需合并

伪代码

//递归法
Function binary-search(A,l,r,v):
	if l = r then
		if A[l] = v then return l;
		else return NIL;
	end
	mid <- [(l+r)/2];
	if v <= A[mid] then return binary-search(A,l,mid,v);
	else return binary-search(A,mid+1,r,v);
end
//非递归法
Function binary-search(A,v):
	l <- 1;
	r <- A.length;
	while l<r do
		mid <- [(l+r)/2];
		if v<= A[mid] then r <- mid;
		else l <- mid + 1;
	end
	if A[l] = v then return l;
	else return NIL;
end

分析

  1. 解空间:解空间表现为一个深度为 O(log⁡n)的递归二叉树,表示在每次迭代中如何通过选择中点来缩小搜索区间。
  2. 时间复杂度:
    共分解2个规模为n/2的子问题,但只有1个子问题要解决,且无需合并。分解所需时间为常数\(\Theta\)(1),由于使用递归实现,所以这个算法总的时间复杂度为O(logn)

快速排序

将数组A[p, ... , r]划分成2个(可能为空)子数组A[p, ... , q-1]和A[q+1, ... , r]使得A[p, ... , q-1]中的每一个元素都小于等于A[q],同时A[q+1, ... , r]中的每一个元素都大于A[q],计算下标q也是划分过程的一部分。递归调用快速排序,排序子数组A[p, ... , q-1]和A[q+1, ... , r],不需要合并

快速排序是原址排序,是实际应用中最优秀的排序算法

伪代码:

//分解方法
Function partition(A,p,r):
	x <- A[r]; //选定中间值
	i <- p - 1; //小区间末位下标
	for j <- p to r - 1 do //大区间末位下标,每次扩容1
		if A[j] <= x then //下一个数小于中间值,该去小区间
			i <- i + 1; //小区间+1
			exchange A[i] with A[j]; //将A[j]交换到小区间
		end
	end
	exchange A[i+1] with A[r]; //将中间值放到中间
	return i + 1; //返回中间值下标
end

//主过程
Function quicksort(A,p,r):
	if(p < r) then
		q <- partition(A,p,r);
		quicksort(A,p,q-1);
		quicksort(A,q+1,r);
	end
end

例题:快速排序

使用一种原址排序的方式对一组数据进行从小到大排序

输入说明

第一行一个整数n(1≤n≤106),表示轻小说的数量。第二行n个用空格分隔的整数,第i个整数ai(−106≤ai≤106)表示第i个数。

输出说明

n个用空格分隔的整数,表示这组数据从小到大排序后的结果。

输入样例

5
9 8 2 5 2

输出样例

2 2 5 8 9

解答

完整代码

using System;

class Test {
    static void Main(string[] args) {
        // 读取输入的小说数量
        int n = int.Parse(Console.ReadLine());
        // 读取小说的编号并转换为整数数组
        string[] input = Console.ReadLine().Split(' ');
        int[] numbers = new int[n];
        for (int i = 0; i < n; i++) {
            numbers[i] = int.Parse(input[i]);
        }
        // 快速排序
        QuickSort(numbers, 0, n - 1);
        foreach (int i in numbers) {
            Console.Write(i + " ");
        }
        Console.ReadLine();
    }
    // 快速排序方法
    static 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); // 递归排序右半部分
        }
    }
    // 分区方法
    static int Partition(int[] arr, int left, int right) {
        int pivot = arr[right];  // 选择最右边的元素作为基准
        int i = left - 1;  // i 指向比 pivot 小的区域的尾部
        for (int j = left; j < right; j++) {
            if (arr[j] <= pivot) // 把小于等于 pivot 的元素放到左边
            {
                i++;
                Swap(arr, i, j);
            }
        }
        // 将 pivot 放到正确的位置
        Swap(arr, i + 1, right);
        return i + 1;
    }
    // 交换两个元素
    static void Swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

分析

  1. 解空间:快速排序的解空间即存储空间的复杂度,主要受到递归调用栈深度的影响。因为快速排序是一个就地排序算法(in-place),所以它并不需要额外的空间来存储数据。快速排序的空间复杂度主要来自递归调用的栈空间。在平均情况下,快速排序的递归深度为 O(log⁡n),因此需要的空间复杂度是:O(logn)
  2. 时间复杂度:分解过程相当于把所有的n个数都访问了一遍,所以分解的复杂度为\(\Theta\)(n)。主过程采用递归实现,递归的深度为O(logn),因此平均时间复杂度为O(nlogn)

标签:策略,递归,int,分治,问题,算法,分解,排序,逆序
From: https://www.cnblogs.com/CloverJoyi/p/18627210

相关文章

  • 每日一算法----设计链表(java)
    classMyLinkedList{staticclassListNode{intval;intindex;ListNodeprev;ListNodenext;publicListNode(intval,intindex,ListNodeprev,ListNodenext){this.val=val;this.in......
  • 避免“大项目”:数字化转型中的智慧与策略
    引言:数字化转型中的“理想与现实”在数字化转型的浪潮中,许多企业都抱着“立马实现全方位的数字化”这一理想,期望通过一个庞大的项目在短期内完成所有变革。但现实却常常是——目标宏大、项目复杂、执行困难,尤其是当项目涉及多个利益相关方时,困难会更为突出。大项目虽然看似能......
  • 2024数据智能|大模型时代的数据管理策略和趋势(附实践资料下载)
    在大模型时代,数据管理面临着新的挑战和机遇。以下是一些关键的数据管理策略和趋势:数据治理:数据治理是确保数据安全、完整、可靠和合规性的关键。它包括数据管理、数据质量控制、数据流程管理等。企业需要建立数据治理框架,明确数据所有权、责任、质量标准及监控机制。数据......
  • 大邻域搜索算法
    大邻域搜索算法(LargeNeighborhoodSearch,LNS)是一种用于求解组合优化问题的启发式算法。以下是对大邻域搜索算法的详细解释:一、基本概念大邻域搜索算法中的“大”指的是邻域变动的范围相对于一般的邻域搜索算法而言更广。该算法的核心思想是在一个比较大的解空间邻域内寻找更好......
  • 变邻域搜索算法
    变邻域搜索算法(VariableNeighborhoodSearch,VNS)是一种基于局部搜索的启发式算法,它通过在不同的邻域结构之间切换来逃避局部最优解,并逐步改进解的质量。以下是对变邻域搜索算法的详细解释:一、算法原理变邻域搜索算法的基本思想是在搜索过程中系统地改变邻域结构集来拓展搜索范围......
  • 每天学习编程两小时(第1天)-图论算法
    学习目标:掌握图论的基本算法(求解每个节点的单源/多源最短路径,求解最小生成树)学习内容:例如:prim算法求解最小生成树Dijkstra算法求解单源最短路径学习时间:2024年12月23日下午学习产出:掌握最小生成树和最短路径的定义和区别掌握prim算法和Dijkstra算法的思想实现......
  • 大数据面试笔试宝典之算法面试
    1.快速排序基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。算法描述:快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:(1)从数列中......
  • 算法刷题Day7_翻转链表
    算法刷题Day7_单链表相关操作文章目录算法刷题Day7_单链表相关操作前言一、反转链表二、两两交换链表中的节点1.递归法2.迭代法总结前言提示:以下是本篇文章正文内容,下面案例可供参考一、反转链表classSolution{public:ListNode*reverseList(ListNode......
  • 算法刷题_删除链表的倒数第N个结点
    算法刷题Day8_删除链表的倒数第N个结点文章目录算法刷题Day8_删除链表的倒数第N个结点前言一、双指针思想二、具体步骤1.定义快慢指针2.fast指针先移动n+1步3.fast和slow一起移动4.删除倒数第N个节点三、完整代码总结前言一、双指针思想双指针的经典应用,如果......
  • 算法设计与分析期末复习
    算法设计与分析期末复习一、选择题1、在计算机科学中,时间复杂度通常用来描述什么?A)算法执行所需的时间B)程序编译所需的时间C)数据传输所需的时间D)内存分配所需的时间 答案:A2、下列哪一项不是衡量算法性能的标准?A)时间复杂度B)空间复杂度C)代码长度D)算......