前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法1.排序二分查找及其变种

算法1.排序二分查找及其变种

作者头像
和蔼的zhxing
发布2018-09-04 11:25:38
8510
发布2018-09-04 11:25:38
举报
文章被收录于专栏:和蔼的张星的图像处理专栏

这个我也不知道能写多少,只是最近快放假了实在懒得看DSP了,而且卡在一个地方了,什么都不干又感觉心慌的很,所以又回头看看算法的东西。一些测试程序放在这里

1.排序

正好看到分治法关于归并排序的这块,所以想把排序做一个总结。主要包括冒泡,计数,插入,选择,归并排序,堆排序和插入排序。写的时候可能没有什么顺序。 排序算法是最常用的一种算法,给定一个数组,把这个数组排序是最基本的需求,所以排序算法有很多种,性能也不移,比较快的堆排序,归并排序和快速排序,其他的都很慢。下面这个表可以看一下:

排序算法

最坏情况下的性能

平均性能

冒泡排序

n^2

n^2

计数排序

n^2

n^2

插入排序

n^2

n^2

选择排序

n^2

n^2

堆排序

nlog(n)

nlog(n)

归并排序

nlog(n)

nlog(n)

快速排序

n^2

nlog(n)

这个是在《数据结构算法与应用-c++描述》的书上的p462,sartaj sahni著。

1.1 归并排序

因为这次我先看的归并排序,就来先写这个了,归并排序是分治法的一个典型应用,其主要思想是把数据分组排序,然后两两一组进行归并(归并的同时对这两组进行排序),直到合并成一个大的数组。

维基百科图片

这个是递归的实现,思路就是分解成单个元素,然后两两归并,无法分组的直接复制下来,直到归并成一个数组,这个时候就是一个排好序的数组了。 为了程序下标简单,我们数组下标从1开始,也就是说0位置空下来,不把数据放进去 先来写一个归并函数,就是把两个排序数组合并成一个排序数组,这个是在归并排序中要经常用到的: l是表示第一个数组的下标起点,m表示第一个数组的最后一个数据下标,n表示第二个数组的最后一个下标。如图,要合并的是绿色和棕色的数组。

代码语言:javascript
复制
void Merge(T *initList, T *mergeList, const int l, const int m, const int n)
{
    //这个是归并两个排序数组,双指针归并
    int i1, i2;    //双指针
    int index = l;    //这个是新数组的序号
    for (i1 = l, i2 = m + 1; i1 <= m&&i2 <=n; index++)
    {
        if (initList[i1] < initList[i2])
        {
            mergeList[index] = initList[i1];
            i1++;
        }
        else 
        {
            mergeList[index] = initList[i2];
            i2++;
        }
    }
    //这两个copy最多只有一个起作用
    copy(initList + i1, initList + m + 1, mergeList + index);
    copy(initList + i2, initList + n + 1, mergeList + index);
}

整体也比较简单,是一个双指针合并数组的标准写法,最后用copy把没有遍历的copy过来就可以了。 上面这个只能实现对一对组合的归并,我们在进行归并的时候可能要对多组数据(这些还是存放在一个大的数组里的)进行归并,所以我们还需要写另外一个函数进行这样的归并。

代码语言:javascript
复制
template<class T>
void MergePass(T *initlist, T *result, int n, int s)   //n是数据个数,s是每段个数
{
    int i;
    for (i = 1; i <=n-2*s+1; i += 2 * s)    //这里一定是n-2*s+1,每两个一对,不成对的话就不能遍历了。
    {
        Merge(initlist, result, i, i + s - 1,i+2*s-1);
    }
    if ((i + s - 1) < n)      //这是最后一次merge才会进入这个循环,也就是i=1下来的
    {
        Merge(initlist, result, i, i + s - 1, n);
    }
    else
        copy(initlist + i, initlist + n + 1, result + i);
}

n是总数据的个数,s是每一段数据的个数,所以里面的for循环是一组一组进行归并,i每次增加2s,因为是两两归并,i的循环条件是<=n-2s+1这个保证i如果等于n-2s+1的时候后面还有2s个数据可以归并。 下面判断i+s-1和n的大小,如果进行了遍历,i+s-1是第一组数据最后的一个数,如果这个数小于n的话,说明已经进行到最后一次归并了,直接对大数组进行两段归并就可以了,这个时候i是等于1的(也就是说并没有进行上面的for循环,n-2s+1已经小于1了,无法分成两组等量的来进行归并)。 如果已经进行了归并,就只把后面的没有分组的复制下来就好了,注意复制的时候是左闭右开区间。

最后就是汇总的程序了,首先是以1为长度归并,然后是2,再是4,以此类推。

代码语言:javascript
复制
template<class T>
void MergeSort(T *initlist, int n)
{
    T *tempList = new T[n + 1];
    for (int l = 1; l < n; l *= 2)
    {
        MergePass(initlist, tempList, n, l);    
        /*copy(tempList, tempList + n + 1, initlist); */   //每次都拷贝一遍,可以重复利用
        //一种更好的写法是下面这个,归并一次变换到原来的数组,而不是简单的拷贝,效率要高一些
        l *= 2;
        MergePass(tempList, initlist, n, l);
    }
    delete[]  tempList;
}

因为不是原位归并,所以需要借助辅助的一个数组,可以每次归并完把数据拷贝回去,一种更好的方式是一次循环里做两次归并。不用担心万一只需要做奇数次归并怎么办,如果如此,最后一次归并实际上是做了一次复制。 归并排序就到这里了。

1.2冒泡排序

这个大概是本科上c++的课程的时候学的第一个算法,算法很简单,每次循环把最大的一个数放到最后面,就相当于冒泡一样,这样循环n次就可以对一组数进行排序: 这里有一个动图可以看一看。

代码语言:javascript
复制
int main(void) {
        int a[]={7,4,9,3,1},temp;
        for(int i=0;i<5;i++)
        for(int j=i;j<5;j++)
        {
                if(a[i]>a[j])
                {
                        temp=a[i];
                        a[i]=a[j];
                        a[j]=temp;
                }
        }
        for(int i=0;i<5;i++)
        cout<<a[i]<<"  ";
        cout<<endl;
        return 0;
}

我从维基上直接复制过来的代码,很简单,不用多说。

1.3插入排序

插入排序也是比较简单的,每次拿出一个数,插入到已经排序好的数组中,插入的过程是一个找位置的过程,具体的过程看这个动图,我们认为数组的第一个数是已经排好序的,然后从第二个数开始验证,为这个数找一个位置。举个简单的例子。

a=[1,3,5,2,1]; 对于这个数组,我们现在遍历到a[3]这个位置。 先用tmp=a[3]把a[3]存储起来,然后tmp依次和a[2],a[1],a[0],比较,如果tmp<a[2],则把a[2]后移(a[3]=a[2])。这个时候数组变成: a=[1,3,5,5,1]; 然后tmp再和a[1]比较,tmp依然小于a[1],则把a[1]后移,则变为: a=[1,3,3,5,1]; 然后tmp和a[0]比较,tmp>=a[0],说明这个位置就是要找的,则把a[1]这个位置赋值为tmp,则变为: a=[1,2,3,5,1]; 这样一次遍历就结束了,其他的都是这样的,注意写对遍历的序号和边界条件就行了。

代码语言:javascript
复制
template<class T>
void InsertSort(T *a, int n)
{
    int i, j;
    for (j = 1; j < n; j++)
    {
        int tmp = a[j];
        cout << "tmp:" << tmp << endl;   //test
        i = j-1;     //已排序的
        while (i >= 0)
        {
            
            if (tmp < a[i])
            {
                a[i + 1] = a[i];   //后移
                i--;
            }
            else
            {
                a[i+1] = tmp;    //找到位置,赋值
                break;
            }
        }
    }
}

二分查找及其变种

  1. 基本二分查找 参考:你真的会写二分查找么 二分查找是一种“猜价格”的最好策略,也很好理解,每一次查找,都把范围缩小一半,直到找到要查找的元素为止,是一种非常高效的查找方式,条件是查找的目标必须是排序的。 基本的二分查找比较简单:
代码语言:javascript
复制
int Binary_Search1(vector<int> &v, int key)
{
        int res=-1;
    int left = 0;
    int right = v.size() - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (key > v[mid])
            left = mid + 1;
        else if (key < v[mid])
            right = mid - 1;
        else
        {
            return mid;      //找到的话返回下标
        }
    }
    return -1;    //表示没有找到
}

两点注意: ①终止条件是<=,如果不加等于,有些数据遍历不到,比如当数据是第一个数时。{1,2,3}查找1的话就会查找不到。 ②left和right的更新是mid+1或者mid-1,这是因为mid已经查找过,而且不这样做的话容易引起死循环。比如当left=right时,mid等于left,这时候无论key的值如何,都会进入left=right的死循环。

如果能够保证key一定存在,查找到就可以返回,这是二分查找最简单的一种写法。

  1. 二分查找的变形 另外,二分查找还存在着一些变形: 比如,当存在多个值时,要求查找最后一个或者第一个,如何处理? 如果找到mid不结束循环,最终的left和right应该是满足这样的关系:left+1=right

示意图

那么,可以找到 ①最后一个小于key的元素,1, ②第一个大于等于key的元素,2, ③最后一个小于等于key的元素,2, ④第一个大于key的元素,5, 另外,如果存在多个key时,稍加判断,就可以找到 ⑤ 第一个与key相等的元素 ⑥最后一个与key相等的元素

首先来看①--④,解决问题的关键在于上面这一张图,如果是左边的话,要求最后指向这么一种状态,那么找到的时候就不能停止查找,而是要把right=mid-1,这样可以让mid继续减少,最后达到左边的图的这一种状态,这时候选择返回left或者right就能达到不同的结果。如果是右边的话就刚好相反,我把代码直接贴到下面:

代码语言:javascript
复制
int Binary_Search2(vector<int> &v, int key);     //查找最后一个小于目标的数
int Binary_Search3(vector<int> &v, int key);     //查找第一个大于等于目标的数
int Binary_Search4(vector<int> &v, int key);      //第一个大于目标的数
int Binary_Search5(vector<int> &v, int key);      //最后一个小于等于目标的数

int Binary_Search2(vector<int> &v, int key)
{
    int left = 0;
    int right = v.size() - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (key > v[mid])
            left = mid + 1;
        else if (key <=v[mid])
            right = mid - 1;
        
    }
    return right;    
}


int Binary_Search3(vector<int> &v, int key)
{
    int left = 0;
    int right = v.size() - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (key > v[mid])
            left = mid + 1;
        else if (key <= v[mid])
            right = mid - 1;

    }
    return left;    
}

int Binary_Search4(vector<int> &v, int key)
{
    int left = 0;
    int right = v.size() - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (key >= v[mid])
            left = mid + 1;
        else if (key < v[mid])
            right = mid - 1;

    }
    return left;    
}

int Binary_Search5(vector<int> &v, int key)
{
    int left = 0;
    int right = v.size() - 1;
    int mid;
    while (left <= right)
    {
        mid = (left + right) / 2;
        if (key >= v[mid])
            left = mid + 1;
        else if (key < v[mid])
            right = mid - 1;

    }
    return right;    
}

⑤⑥的问题其实就已经在上面了包括了,如果存在多个key值,第一个大于等于key值的元素就是第一个key值,最后一个小于等于key值得元素就是最后一个key值。

另外,如果key值本身就不存在,我们想找一个可以插入key值得位置,那么我们既可以找最后一个小于等于key值的索引,插到其后,也可以找第一个大于key值得索引,插到它前面。 理解了这些再去看二分插入简直不要太简单,看这个题:最长上升子序列

未完待续

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.02.02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.排序
  • 二分查找及其变种
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档