【一文吃透归并排序】基本归并·原地归并·自然归并 C++

news2024/11/29 0:53:49

目录

  • 1 引入情境
    • 基本归并排序实现 C++
  • 2 原地归并排序
    • 2-1 死板的解法
    • 2-2 原地工作区
    • 2-3 链表归并排序
  • 3 自底向上归并排序
  • 4 两路自然归并排序
    • 4-1 形式化描述
    • 4-2 代码实现

1 引入情境

  归并思想:假设有两队小孩,都是从矮到高排序,现在通过一扇门后合并为一队。要求:任何孩子,只有所有比它矮的孩子通过后,才能通过。

  直接给出归并排序的伪代码:
function merge_sort(A):
	if len(A) > 1 then
		m = floor(len(A)/2)  # 获取中心点
		X = copy_array(A,0,m)
		Y = copy_array(A,m+1,len(A)-1) #拆成差不多的两半
		merge_sort(X)
		merge_sort(Y) # 分别排序
		merge(A,X,Y) # 合并

  假如我理解了上面归并,但是,在排序之前,怎么得到有序的两个队列呢?注意到,伪代码的出口是 l e n ( A ) ≤ 1 len(A) \leq 1 len(A)1 就会返回,即一个数本身有序。当它拆分到自然有序,返回头开始合并,直接上图:

   一个更明确的例子,同色表明为一组,且基本是平均分割:

  只剩下一个问题,merge(A,X,Y)是怎么做的?简单的理解,每次比较X,Y头部,选一个最小的放到目标有序数组A的队尾即可;若一队为空,另一队还有剩余就直接放到A的队尾。给出伪代码描述:

function Merge(A,X,Y):
	i=1,j=1,k=1
	xlen=|X|
	ylen=|Y|
	# 从队伍头部选一个最小的放到目标队伍末尾
	while i<= xlen and j <=ylen do
		if X[i] <= Y[i] then
			A[k] = X[i]
			i = i + 1
		else
			A[k] = Y[j]
			j = j + 1
		k = k + 1
	# 处理剩余的部分,放到末尾
	while i <= xlen do
		A[k] = X[i]
		k = k + 1
	while j <= ylen do
		A[k]= Y[j]
		j = j + 1

   关于时间复杂度,设排序用时为 T ( N ) T(N) T(N),则递归开销为:
T ( N ) = T ( N 2 ) + T ( N 2 ) + c N T(N) = T(\frac {N} {2})+T(\frac {N} {2})+cN T(N)=T(2N)+T(2N)+cN

   对两半分别排序是 2 T ( N 2 ) 2T(\frac {N} {2}) 2T(2N) c n cn cn是合并两个队列用时,解开后是O(NlgN)。另一种直观理解的方式如下图,拆半分层最多是 l g N lgN lgN层,每层归并都是O(N),相乘即可。

基本归并排序实现 C++

   在实现之前,我们加一个小改进,在每队队伍加入一个哨兵:正无穷大(如果是非递增是负无穷),于是,merge的过程无需另行考虑剩余部分。

# 加入正无穷哨兵,会让一个较短的队列的下标阻塞在末尾
function merge(A,X,Y):
	append(X,INF) # 在队列尾加入哨兵,正无穷INF
	append(Y,INF)
	for k from 1 to A.len do
		if X[i] < Y[j] then
			A[k]=X[i]
			i = i + 1
		else
			A[k]=Y[j]
			j =j + 1 		

   实现时发生了两个错误:

  • l+(r-l)>>1l+(r-l)/2是不同的;l+(r-l)/2l+((r-l)>>1)才等价
  • l 和 1 总是容易弄混;下载编程字体点链接source code Pro
#include <iostream>
#include <vector>
#include <string>
using namespace std;

const int INF=1e5;
using Key=int;
using Array =vector<Key>;

void merge(Array &A,int l,int m,int r){
	// 开两个队列分别放 A[l,m) A[m,r)
	Array X(m-l+1);//多一个哨兵的位置
	Array Y(r-m+1);

	//复制元素
	int i=0,j=0;
	for(int k=l;k<m;++k) X[i++]=A[k];
	for(int k=m;k<r;++k) Y[j++]=A[k];

	X[m-l]=INF; //设置哨兵
	Y[r-m]=INF;

	for(i=0,j=0;l<r;++l){
		A[l]=X[i] < Y[j] ? X[i++]:Y[j++];
	}
}

// A[l,r)
void merge_sort(Array &A,int l,int r){
	if(r-l>1){
		int m = l+(r-l)/2; // 防止溢出
		merge_sort(A,l,m);
		merge_sort(A,m,r);
		merge(A,l,m,r);
	}
}

static void see_array(string arr_info,const Array &A){
	cout<<arr_info <<" { ";
	for(auto& item:A){
		cout<<item<<" ";
	}
	cout<<"}"<<endl;
}
void test(){
	Array arr{23,45,1,2,7,5,6};
	// merge(arr,0,3,arr.size());
	merge_sort(arr,0,arr.size());

	see_array("sorted ",arr);
}

int main()
{
	test();
	return 0;
}

  注意到,在merge中需要申请两个数组来放数据,频繁的申请释放内存会花费大量的时间。一个解决办法是一次性申请一个和待排序数组一样大的数组B,作为工作区。这一改进将所需空间从O(NLgN)减少到了O(N),对十万个数字排序上速度提示20%~25%。C++实现如下:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

const int INF=1e5;
using Key=int;
using Array =vector<Key>;

void merge(Array &A,Array &B,int l,int m,int r){
	// 合并 A[l,m) A[m,r) 到 B,再复制回来
	int i,j,k;
	// 记录两个队列的起点
	i=k=l;
	j=m;

	// 归并
	while(i<m && j<r){
		B[k++]=A[i]<A[j] ? A[i++]:A[j++];
	}
	// 收尾工作
	while(i<m) B[k++]=A[i++];
	while(j<r) B[k++]=A[j++];

	for(;l<r;++l) A[l]=B[l];
}

void msort(Array &A,Array &B,int l,int r){
	int m;
	if(r-l>1){
		m = l+(r-l)/2; // 防止溢出
		msort(A,B,l,m);
		msort(A,B,m,r);
		merge(A,B,l,m,r);
	}
}

// A[l,r)
void merge_sort(Array &A,int l,int r){
	Array B(r-l);  //申请一个同样大小的工作区
	msort(A,B,l,r);
}

static void see_array(string arr_info,const Array &A){
	cout<<arr_info <<" { ";
	for(auto& item:A){
		cout<<item<<" ";
	}
	cout<<"}"<<endl;
}
void test(){
	Array arr{23,45,1,2,7,5,6};
	// merge(arr,0,3,arr.size());
	merge_sort(arr,0,arr.size());
	see_array("sorted ",arr);
}

int main()
{
	test();
	return 0;
}

2 原地归并排序

  不带优化的基本实现需要空间O(NLgN),使用工作区也需要O(N),可以不用额外空间原地排序吗?

2-1 死板的解法

  如下图,类比插入排序,通过比较 x i x_i xi x j x_j xj的大小确定把谁放在当前 x i x_i xi的位置上;注意到如果 x j x_j xj是更小的那个,需要把第一个序列整体向后移动一步,即覆盖掉 x j x_j xj,这样一来归并排序降低为 O ( N 2 ) O(N^2) O(N2)

  修改后的归并方法如下:
void naive_merge(Array &A,int l,int m,int r){
	int i;
	Key tmp;

	for(;l<m && m<r;++l){
		if(!(A[l]<A[m])){
			tmp=A[m++]; //先保存,后覆盖
			for(i=m-1;i>l;--i) A[i]=A[i-1];
			A[l]=tmp;
		}
	}
}

2-2 原地工作区

   注意,这一节有亿点点的抽象,看不懂可以跳过。原地工作区的思路是借用数组未排序的部分复用为归并的工作区,稍作思考就会发现有几个问题:

  1. 工作区一般是空白的,如果和未排序元素共用,会不会直接把原来的、还没有排序的数据给覆盖,造成数据丢失?

  2. 有序的子序列很大,自然意味着剩余未排序的空间小,那用这么小的工作区能完成归并任务吗?是不是和有序的子数组也会重叠呢?

  先记下这些问题并直接给出方案,并逐渐改进使问题得到应对。

  核心操作就是交换未排序的数据到子序列中,伪代码描述:

function merge(A,[i,m),[j,n),k):
    while i<m and j <n do 
        if A[i]<A[j] then
            swap(A[k],A[i])
            i =i+1
        else 
            swap(A[k],A[j])
            j =j+1
        k=k+1
    while i<m do
        swap(A[k],A[i])
        i=i+1
        k=k+1
    while j<n do
        swap(A[k],A[j])
        j=j+1
        k=k+1

  考虑问题2,在工作区>=待归并元素长的条件下,容易实现数组的一半有序,因为可以拿另一半作为工作区。接下来的问题是,剩下的一半怎么搞?再递归呢?如果直接归并1/4和1/2,未排序的复用空间明显不够用。

  实际上,工作区可以和任何一个有序的子数组重叠,但是需要保证,尚未归并的元素不会被覆盖。所以,当归并1/4和1/2的元素时,我们可以把工作区放在正中间,允许它和有序的子数组重叠。

  接下来,我们看两个极端的例子:

   其他正常的例子与特殊情况均可保证工作区被换到左侧。于是算法明确了:总是重复对未排序部分的前1/2进行排序,从而将有序的结果交换到前一半,而使得工作区位于中间。渐渐当工作区足够小,剩余一个元素等价于插入排序;实际上对于最后几个元素(20以内)完全可以直接插入排序。伪代码描述如下:

function sort(A,l,r)
	if r-l>0 then
		m = floor((l+r)/2)
		w = l+r-m # 工作区的起点
		sort1(A,l,m,w)  # 保证后一半元素有序
		while w -l >1 do # 工作区间不为空
			nr = w
			w = ceil((l+nr)/2)
			sort1(A,w,nr,l) # 同样递归对后1/2排序后放到前面
			merge(A,[l,l+nr-w],[nr,r],w)
		## 改用插入排序
		for i from w down-to l do
			j =i
			while j <= r and A[j]<A[j-1] do
				swap(A[j],A[j-1])
				j=j+1

#反向调用sort 用于交换工作区和有序部分
function sort1(A,l,r,w)
	if r-l > 0 then
		m = floor((l+r)/2)
		sort(A,l,m)
		sort(A,m+1,r)
		merge(A,[l,m],[m+1,r],w) #排序并放到w开始的位置
	else #将所有元素交换到工作区
		while l<=r do
			swap(A[l],A[w])
			l =l+1
			w =w+1

  注意虽然实现了原地归并,即O(1)的空间复杂度,但多了不少额外的交换操作。和使用额外工作区的版本相比,对十万个数字排序,速度下降了60%。C++ 详细实现如下:

#include <iostream>
#include <cmath>
#include <ctime> //随机数组

using namespace std;

using Key = int;

// 定于一个区间
struct Range
{
    int l, r;
    Range(int a, int b) : l(a), r(b) {}
    void set(int left, int right)
    {
        l = left;
        r = right;
    }
    int size()
    {
        return r - l;
    }
}; //[l,r);

// 封装C语言的数组
struct Array
{
    Key *arr;
    int len;
    
    Array(int l) : len(l)
    {
        arr = (Key *)malloc(sizeof(Key) * len);
        srand(time(NULL));
        static const int M=15; //在1~M范围内生成随机数
        for (int i = 0; i < len; i++)
        {
            at(i) = rand() % M + 1;
        }
    }
    Key &operator[](int i)
    {
        return *(arr + i);
    }
    Key &at(int i)
    {
        return *(arr + i);
    }
    void swap(int i, int j)
    {
        Key tmp = at(i);
        at(i) = at(j);
        at(j) = tmp;
    }
    void exchange(Array &B)
    {
        Key *tmp = arr;
        arr = B.arr;
        B.arr = tmp;
    }
    void show(const char info[]=""){
        cout<<info<<"[";
        for (size_t i = 0; i < len; i++)
        {
            cout<<at(i)<<" ";
        }
        cout<<"]."<<endl;
        
    }
};

/// @brief 将数组A的两个区间的内容归并到start开始的区域
/// @param A 原数组
/// @param X 原数组的一个区间
/// @param Y 另一个区间
/// @param start 存放归并结果的开始位置
void merge(Array &A, Range &X, Range &Y, int start)
{
    while (X.l < X.r && Y.l < Y.r)
    {
        int now = A[X.l] < A[Y.l] ? X.l++ : Y.l++;
        A.swap(start++, now);
    }

    while (X.l < X.r)
        A.swap(start++, X.l++);
    while (Y.l < Y.r)
        A.swap(start++, Y.l++);
}

void sort(Array &A, int l, int r, int w);
void merge_sort(Array &A, int l, int r)
{
    int m, n, w;

    if (r - l > 1)
    {
        m = l + (r - l) / 2; // mid
        w = l + r - m;       // 工作区的终点+1
        sort(A, l, m, w);    // 将前一半排序放到后一半

        //[l,w]当前待排序的区间
        while (w - l > 2)
        {
            n = w;
            w = l + (n - l + 1) / 2;
            sort(A, w, n, l); // 排序后一半放到前面
            Range X(l, l + n - w), Y(n, r);
            merge(A, X, Y, w); // 将两头有序数组合并
        }

        // 剩下一两个数作插入排序
        for (n = w; n >= l; --n)
        {
            for (m = n; m < r && A[m] < A[m - 1]; ++m)
            {
                A.swap(m, m - 1);
            }
        }
    }
}

/// @brief 把A[l,r)排序后,放到从下标w开始的区域
void sort(Array &A, int l, int r, int w)
{
    int m;

    if (r - l > 1)
    {
        m = l + (r - l) / 2;
        merge_sort(A, l, m);
        merge_sort(A, m, r);
        Range X(l, m), Y(m, r);
        merge(A, X, Y, w);
    }
    else
    {
        while (l < r)
        {
            A.swap(l++, w++);
        }
    }
}

void test()
{
    int len = 8;
    Array A(len);
    A.show("srcArr ");

    // test sort
    //  sort(A,0,3,3);

    merge_sort(A, 0, len);
    A.show("sorted ");
}

int main()
{
    test();
    return 0;
}

2-3 链表归并排序

  和数组存储类似,链表也需要均分为两个差不多长的子序列,这里使用一种奇偶分割法,简单说,从原链表头部摘下节点,交替放到两个不同的子链表中。伪代码说明:

function split(L):
	(A,B) =(null,null)
	while L not null do
		ptr = L # 当前头结点
		L = next(L) # 头指针后移
		A = link(p,A) #将p放到链表A的头部
		swap(A,B) # 交换,下次换B被插入新元素
	return (A,B)

  C++ 详细实现如下:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

const int INF=1e5;
using Key=int;
using Array =vector<Key>;

struct Node{
	Key val;
	struct Node* nxt;
	Node(Key k,struct Node* next=nullptr):val(k),nxt(next){}
};

using Nptr=struct Node* ;

Nptr link(Nptr pre,Nptr old_head){
	pre->nxt=old_head;
	return pre;
}

Nptr mk_list(const Array &A){
	Nptr head=new Node(-1);
	Nptr p=head;
	for(auto &x:A){
		Nptr now=new Node(x);
		p->nxt=now;
		p=now;
	}
	return head->nxt;
}


Nptr merge(Nptr ap,Nptr bp){
	Nptr head=new Node(-1);//哨兵,头结点
	Nptr p=head;
	while(ap && bp){
		if(ap->val<bp->val){
			link(p,ap);
			ap=ap->nxt;
		}else{
			link(p,bp);
			bp=bp->nxt;
		}
		p=p->nxt;
	}
	if(ap) link(p,ap);
	if(bp) link(p,bp);

	return head->nxt;
}

Nptr msort(Nptr L){
	Nptr p, ap,bp;
	if(!L || !L->nxt) return L;

	ap=bp=nullptr;
	while(L){
		p=L;
		L=L->nxt;
		ap=link(p,ap);
		swap(ap,bp);
	}

	ap=msort(ap);
	bp=msort(bp);
	return merge(ap,bp);
}

void test_list(){
	Array A{23,7,1,56,23,8,31};
	Nptr l=mk_list(A);
	l=msort(l);

	Nptr p=l;
	while(p){
		cout<<p->val<<endl;
		p=p->nxt;
	}
}

int main()
{
	test_list();
	return 0;
}

3 自底向上归并排序

  不从整体开始切分,而是一开始把每一个元素当成一个列表,然后,自底向上地合并相邻的列表,重复利用开始的那张图。注意到,因为不同切分数组,可以直接迭代无需递归

  • 首先获得N个子列表,每个子列表一个元素;
  • 将相邻的两个子序列合并,得到 n / 2 n/2 n/2个长度为2的子序列;重复这个过程,直到整体有序。

  伪代码描述:

function sort(A):
	B = {} # 存放列表的列表,设下标从1开始
	for each a in A do
		B.append({a})   # 存入N个长度为1的子列表
	
	N = B.length
	while(N>1){
		for i from 1 to floor(N/2) do
			B[i] = merge(B[2i-1],B[2i]) # 每次取头两个合并
		if Odd(N) then
			B[ceil(N/2)] = B[N] # 处理落单的
		N = ceil(N/2) # 向上取整
	if B is empty then
		return {}
	return B[1]

  严蔚敏的《数据结构(C语言版)》给出一种实现如下,写得真好:

/********************************************************
*函数名称:Merge
*参数说明:pDataArray 无序数组;
 *          int *pTempArray 临时存储合并后的序列
 *          bIndex 需要合并的序列1的起始位置
 *          mIndex 需要合并的序列1的结束位置
                  并且作为序列2的起始位置
 *          eIndex 需要合并的序列2的结束位置
*说明:    将数组中连续的两个子序列合并为一个有序序列
*********************************************************/
void Merge(int* pDataArray, int *pTempArray, int bIndex, int mIndex, int eIndex)
{
	int mLength = eIndex - bIndex;    //合并后的序列长度
	int i = 0;    //记录合并后序列插入数据的偏移
	int j = bIndex;    //记录子序列1插入数据的偏移
	int k = mIndex;    //记录子序列2掺入数据的偏移
 
	while (j < mIndex && k < eIndex)
	{
		if (pDataArray[j] <= pDataArray[k])
		{
			pTempArray[i++] = pDataArray[j];
			j++;
		}
		else
		{
			pTempArray[i++] = pDataArray[k];
			k++;
		}
	}
 
	if (j == mIndex)    //说明序列1已经插入完毕
		while (k < eIndex)
			pTempArray[i++] = pDataArray[k++];
	else                //说明序列2已经插入完毕
		while (j < mIndex)
			pTempArray[i++] = pDataArray[j++];
 
	for (i = 0; i < mLength; i++)    //将合并后序列重新放入pDataArray
		pDataArray[bIndex + i] = pTempArray[i];
}
 
/********************************************************
*函数名称:BottomUpMergeSort
*参数说明:pDataArray 无序数组;
 * 	   iDataNum为无序数据个数
*说明:    自底向上的归并排序
*********************************************************/
void BottomUpMergeSort(int* pDataArray, int iDataNum)
{
	int *pTempArray = (int *)malloc(sizeof(int) * iDataNum);    //临时存放合并后的序列
	int length = 1;    //初始有序子序列长度为1
	while (length < iDataNum)
	{
		int i = 0;
		for (; i + 2*length < iDataNum; i += 2*length)
			Merge(pDataArray, pTempArray, i, i + length, i + 2*length);
		if (i + length < iDataNum)
			Merge(pDataArray, pTempArray, i, i + length, iDataNum);
		length *= 2;    //有序子序列长度*2
	}
	free(pTempArray);
}

  几点说明:

  • length代表的是单个子序列的长度;
  • pTempArray 是上述文章提到的统一的工作区
  • 实际排序时,并没有分为多个数组,而是只针对下标作逻辑上的合并,数据还在原来的数组中。
    • 每次需要合并的两部分是 A[i,i+length) 和 A[i+length,i+2*length)注意右边界取不到。
    • 下一次只需要把长度值翻倍即可,省去了许多开辟、释放空间的时间。

4 两路自然归并排序

  从一个例子说起,如下图。虽然整个序列是混乱的,但是总有些部分是自然有序的(最差是一个数);想象一个两头都被点燃的蜡烛,同样的,我们从序列的两头同时开始寻找一个非递减的序列,如图中左右两端的 [8,12,14]和[15,7],合并到最右端;下一次找到后,合并到最最左端,这样做是为了平衡。

  注意,上图数据归并后,左边是非递减,右边是非递增;且并不是一次完成所有的数据有序,而是多段有序,下面会进一步解释。

4-1 形式化描述

  自然归并排序的不变性质,如下图所示:

  1. 任何时候,a之前和d之后的元素已经被扫描并归并到工作区的左右两端。
  2. 每次都需要将非递减子序列[a,b)扩展到最长,同样的将[c,d)向左扩展到最长。
  3. f(front)之前和r(rear)之后的元素是已经处理过的,可能包含若干有序的子序列,并非直接有序。
  4. 奇数轮归并到f这一边,偶数轮归并到r那一边。
  5. 关键中的关键,当扫描一轮后,原数组和工作区交换,继续归并,此时原数组(指针)指向工作区,且有序的子序列明显增多。

  给出伪代码描述,如果还是看不明白建议抄一下伪代码:

function sort(A):
	if A.length > 1 then
		n = A.length
		B = creat-array(n) # 创建工作区
		loop 
			[a,b) = [1,1)
			[c,d) = [n+1,n+1)
			f = 1,r = n  # 工作区的首尾指针
			t = False # 控制从f还是r归并
			while b < c do
				repeat 
					b = b+1
				until b >= c || A[b]<A[b-1]
				repeat
					c = c-1
				until c<=b || A[c-1]<A[c]
				if c < b then # 避免overlap 重叠
					c = b
				if b-a >=n then # 整个数组已经有序
					return A
				if t then # 从front归并
					f = merge(A,[a,b),[c,d),B,f,1)
				else #从rear归并
				 	r = merge(A,[a,b),[c,d),B,r,-1)
				a = b
				d = c
				t = not t # 换方向
			exchange A<->B   # 关键的一步,切换工作区
		return A

# w 是工作区归并边界的下标 f 或 r
# det 表示方向 +1 或 -1
function merge(A,[a,b),[c,d),B,w,det):
	while a<b and c<d do
		if A[a]<A[d-1] then
			B[w] = A[a]
			a =a + 1
		else
			B[w] = A[d-1]
			d = d-1
		w = w+det

		while a < b do
			B[w] = A[a]
			a=a+1
			w=w+det	

		while c < d do
			B[w]=A[d-1]
			d=d-1
			w=w+det
	return w

4-2 代码实现

  一些说明:

  • 为了提高可读性,封装了C语言下的数组指针和区间类Range
  • 参数很多时,建议先声明,然后统一赋值;注意到下标得到赋值在每一轮都需要更新
#include <iostream>
#include <cmath>
#include <ctime> //随机数组

using namespace std;

using Key = int;

// 定义一个左闭右开的区间
struct Range{
    int l,r;
    void set(int left,int right){
        l=left;
        r=right;
    }
    int size(){
        return r-l;
    }
};//[l,r);

// 封装C语言的数组
struct Array
{
    Key* arr;
    int len;
    Array(int len):len(len){
        arr=(Key*) malloc(sizeof(Key)*len);
    }
    Key& operator[](int i){
        return *(arr+i);
    }
    Key& at(int i){
        return *(arr+i);
    }
    void exchange(Array &B){
        Key* tmp=arr;
        arr=B.arr;
        B.arr=tmp;
    }
};


/// @brief 合并A的两个区间 Up Down到工作区B的w开始处
/// @param A 源序列
/// @param Up 位于左边的非递减序列
/// @param Down 位于右边的非递增序列
/// @param B 工作区,和源序列同样大
/// @param w 工作区的归并起点
/// @param det 控制w的增长方向,+1 or -1
/// @return 归并后的w值
int merge(Array &A, Range &Up, Range &Down,
          Array &B, int w, int det)
{
    for(;Up.l<Up.r && Down.l<Down.r;w+=det){
        B[w]= (A[Up.l]<A[Down.r-1]) ? A[Up.l++]:A[--Down.r];
    }
    // 收尾工作
    for(;Up.l<Up.r;w+=det){
        B[w]=A[Up.l++];
    }
    for(;Down.l<Down.r;w+=det){
        B[w]=A[--Down.r];
    }

    return w;
}

Array& merge_sort(Array &A){
    if(A.len<2) return A;

    Array B(A.len); //申请工作区
    Range Up,Down; //左边的非递减区间,右边的非递增区间
    int front,rear,dir;//工作区的前后指针和方向

    for(;;){
        Up.set(0,0);
        Down.set(A.len,A.len);
        front=0;
        rear=A.len-1;
        dir=1;

        while(Up.r<Down.l){
            do{
                ++Up.r; // 寻找非递减序列的右边界
            }while(Up.r<Down.l && A[Up.r-1]<=A[Up.r]);
        
            do{
                --Down.l;
            }while(Up.r<Down.l && A[Down.l]<=A[Down.l-1]);

            if(Down.l<Up.r){
                Down.l=Up.r; //消除可能的重叠
            }
            // 出口,已经有序
            if(Up.size()>=A.len){
                return A;
            }
            if(dir){
                front=merge(A,Up,Down,B,front,1);
            }else{
                rear=merge(A,Up,Down,B,rear,-1);
            }

            Up.l=Up.r;
            Down.r=Down.l;// 跳过已经扫描的部分
            dir=!dir;
        }
        A.exchange(B);
    }
    return A;
}

void test(){
		const int  len=7;
    Array A(len);
    srand(time(NULL));
    for(int i=0;i<len;i++){
        A.at(i)=rand()%25+1;
        cout<<A.at(i)<<" ";
    }
    cout<<endl;

    merge_sort(A);
    for(int i=0;i<len;i++){
        cout<<A.at(i)<<" ";
    }
    cout<<endl;
}

int main()
{
    test();
    return 0;
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/524148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微服务#3 Docker常用命令

Docker 一.初识docker Docker如何解决大型项目依赖关系复杂&#xff0c;不同组件依赖的兼容性问题? Docker允许开发中将应用、依赖、函数库、配置一起打包&#xff0c;形成可移植镜像Docker应用运行在容器中&#xff0c;使用沙箱机制&#xff0c;相互隔离 Docker如何解决开…

在树莓派上搭建WordPress博客网站,并内网穿透发布到公网

✨个人主页&#xff1a;bit me&#x1f447; 目 录 &#x1f43e;概述&#x1f490;安装 PHP&#x1f338;安装MySQL数据库&#x1f337;安装 Wordpress&#x1f340;设置您的 WordPress 数据库&#x1f339;设置 MySQL/MariaDB&#x1f33b;创建 WordPress 数据库 &#x1f33…

pga_aggregate_limit和process关系

之前部署19c时&#xff0c;配置pga_aggregate_limit都是直接配置成0了&#xff0c;配置processes的大小也比较随意&#xff0c;上周维护一个客户安装的环境&#xff0c;重启数据库数据库时告警了&#xff0c;才第一次认真对面了 SYSorcl1> startup ; ORA-00093: pga_aggreg…

鼠标拆卸方法、鼠标按键噪声大解决办法(为按钮加润滑脂)

文章目录 今天我买了一款acer鼠标&#xff0c;发现按键声音有点大&#xff0c;想给它的按钮上一点缓冲脂&#xff0c;这个缓冲脂本来我是买来润滑机械键盘轴体用的&#xff0c;刚好也能用上。 这是我的鼠标外壳结构&#xff0c;我拆卸的时候没注意&#xff0c;给我拆坏了&…

SRP中的shader

SRP中的shader 在Unity中使用SRP时&#xff0c;我们需要使用HLSL来编写shader。HLSL语法与GLSL类似&#xff0c;在使用HLSL的地方我们需要用HLSLPROGRAM和ENDHLSL来包裹&#xff1a; Shader "My Pipeline/Unlit" {Properties {}SubShader {Pass{HLSLPROGRAM#pragma …

MySQL访问方法

访问方法&#xff08;访问类型&#xff09; Const 通过主键或者唯一二级索引来定位到唯一一条满足条件的数据的访问方法称为const。特别指出&#xff0c;如果主键中有两个列l1和l2&#xff0c;查询条件中只有l1的话访问方法就不是const&#xff0c;因为通过l1并不能确定唯一一条…

VisualStudio如何配置PCL点云库?

文章目录 0.引言1.准备PCL点云库2.VisualStudio相关设置3.配置PCL点云库4.测试PCL 0.引言 因笔者课题涉及点云处理&#xff0c;需要通过PCL进行点云数据分析处理&#xff0c;查阅现有网络资料&#xff0c;实现了VisualStudio2015(x86)配置PCL1.8.1点云库&#xff0c;本文记录实…

Python——pyqt-auto-tool自动化小工具(源码)

目录 一、演示视频 二、代码包 三、使用步骤 四、总结 一、演示视频 pyqt-auto-tool自己乱改的自动化小工具_哔哩哔哩_bilibili今天教资考试完&#xff0c;花半天自己乱改的自动化小工具pyqt-auto-tool&#xff0c;二开1、auto-tool原作者&#xff1a;https://www.bilibil…

学习通项目需要用到的

思路&#xff1a; 把长连接弄好课堂详细页面可以看到班级的所有学生&#xff0c;双击学生弹窗&#xff0c;可以确认从该课程中移除学生教师可以在班级详细页面通过输入学生的id&#xff0c;将学生加入该班级做好签到的页面&#xff0c;输入签到标题&#xff0c;选择签到的结束…

玩转计算机组成原理--指令系统

目录 一、机器指令 1.1指令的一般格式 1.操作码: 2.地址码 1.2指令字长 二、操作数类型和操作类型 2.1操作数类型 2.2操作类型 三、寻址方式(重要) 3.1指令寻址 1.顺序寻址 2.跳跃寻址 3.2数据寻址 1.立即寻址 2.直接寻址 3.隐含寻址 4.间接寻址 5.寄存器寻址 …

创作星-创意大爆发!AI文案生成器让创作轻松快捷,轻松撰写出热门标题。

一、创作星-创意大爆发&#xff01;AI文案生成器让创作轻松快捷&#xff0c;轻松撰写出热门标题。 ✨使用“创作星”&#xff0c;让AI帮你生成惊艳的文案&#xff01; ✨创意大爆发&#xff01;AI文案生成器让创作轻松快捷&#xff0c;轻松撰写出热门标题。 ✨AI文案神器&…

【Linux指令④】Linux查看日历、时间、打包压缩文件、Linux中的计算器,常用热键 关机指令

Linux指令 ❀date❀cal❀findwhichwhereis ❀grep❀zip/unzip❀tar❀bc❀常用的热键❀关机&#x1f340;小结&#x1f340; &#x1f389;博客主页&#xff1a;小智_x0___0x_ &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f389;系列专…

kubectl常用命令|pod生命周期|金丝雀发布|超详细

kubectl常用命令|pod生命周期|金丝雀发布|超详细 一 kubectl常用命令1.1 查看版本信息1.2查看资源对象简写1.3查看集群信息1.4 配置kubectl自动补全1.5node节点查看日志 二 k8s基本信息查看2.1 查看 master 节点状态2.2 查看命令空间2.3 创建和删除命名空间2.4 在命名空间kube-…

python读写json文件方法详解

在我们日常使用 Python时&#xff0c;经常会使用到 json文件。那么在平时写一些小程序时&#xff0c;如何使用 json文件呢&#xff1f;今天我将介绍如何读取和写入 Json文件。 json是一种数据结构&#xff0c;它是将字符串转换成数据的一种技术。使用 json可以非常方便的将一组…

在CentOS上安装Docker的步骤

在CentOS上安装Docker的步骤如下&#xff1a; 更新系统&#xff1a; sudo yum update 添加Docker官方存储库&#xff1a; sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum-config-manag…

FL 3D segment

HarmoFL&#xff1a;在异构医学图像联邦学习中协调本地和全局漂移 多个医疗机构联合使用联邦学习&#xff08;FL&#xff09;训练模型已成为最大化数据驱动模型潜力的有前途的解决方案&#xff0c;但医学图像中的非独立和非同分布&#xff08;non-iid&#xff09;数据仍然是实…

支付系统设计三:渠道网关设计03-参数验证

文章目录 前言一、参数校验1. MessageDescription内容2. 参数验证执行器3. 字段验证处理器4. 验证器工厂5. style校验器6. 验证器接口7. 长度校验器8. 枚举类型校验器9. yml配置9.1 deduct.yml9.2 style.yml 二、幂等校验总结 前言 在《支付系统设计三&#xff1a;渠道网关设计…

linux内核编译不通过问题的两种排查方法(非正式)

目录 一、前言 二、 问题现象 三、问题排查思路1&#xff1a;问题所在处向上排查 3.1 整体思路 3.2 排查过程 3.3 资料查询 3.4 尝试动作1&#xff1a;开启相应配置项 3.4.1 检查内核配置项 3.4.2 开启配置项 3.4.3 尝试编译 四、问题排查思路2&#xff1a;从上向下排…

讯飞星火大模型体验报告

近日&#xff0c;科大讯飞召开了星火认知大模型成果发布会&#xff0c;会上表示讯飞星火大模型将突破开放式问答&#xff0c;对标ChatGPT&#xff0c;在中文能力上超过ChatGPT&#xff0c;在英文能力上与ChatGPT相当。对此&#xff0c;你怎么看&#xff1f; 笔者准备给bing/ch…

数据结构第二天:File Transfer 【树的应用:集合】

原题是英文的&#xff1a;题目详情 - 05-树8 File Transfer (pintia.cn) 我用软件翻译了一下: 我们有一个计算机网络和一系列双向连接。这些连接中的每一个都允许文件从一台计算机传输到另一台计算机。有没有可能从网络上的任何一台计算机向其他计算机发送文件? Input Specif…