Algorithm Review 1 基础排序算法

整天做上层架构设计和写界面,把计算机最重要的算法与数据结构都忘得差不多了。所以从这篇开始系统地复习常见的算法与数据结构,这里会暂时抛弃Java,用C++来做,因为即使是Android系统,算法实现也大多是通过C或者C++编译成so来实现的。对于算法的描述我会尽量抛弃复杂的理论描述,尽量用大白话来让大家好理解。
第一篇是关于普通数组排序的,默认排序都是从小到大~

一、冒泡排序
算法复杂度n^2
把数组竖起来,尾部想象成一个水面,每次让最大的元素浮到水面上,就像冒泡一样。
基本思想就是每次两两数据比较,如果左边元素大于右边元素,则交换两者,这样每次都能让一个最大的元素浮上“水面”。

void bubbleSort(int arr[], int length)
{
    for(int i = 0; i < length; i++)
    {
        for(int j = 0; j < length - i -1; j++)
        {
            if(arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

注意:C++内置有swap函数,因为涉及到内存的读写所以效率较低,有兴趣的同学可以用同样的算法进行测试,在数据量超过100000时,算法效率会大幅下降,所以本文中不使用该函数。

二、选择排序
算法复杂度n^2
假设每次数组的第一个元素的索引是指向的最小的元素,然后用这个元素和后面所有的元素进行比较,如果后面元素有比这个元素小,就把这个假设的索引值改成这个较小元素的索引,让后继续往后搜索直到数组末尾,每次选出来的就是那个最小的元素的索引,然后让第一个索引和这个索引交换数据即可。

void selectionSort(int arr[], int length)
{
    for(int i = 0; i < length; i++)
    {
        int minIndex = i;
        for(int j = i + 1; j < length; j++)
        {
            if(arr[j] < arr[minIndex])
            {
                minIndex = j;
            }
        }

        if(minIndex != i)
        {
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }
}

三、插入排序
算法复杂度n^2
每次将后面一个元素插入到前面已经排序好的数组的正确位置,所以插入排序是从i = 1开始的,因为只有一个元素的时候它本身就是有序的。

void insertionSort(int arr[], int length)
{

    for (int i = 1; i < length; i++)
    {
        for (int j = i - 1; j >= 0; j--)
        {
            if(arr[j] > arr[j + 1])
            {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

void insertionSortImprove(int arr[], int length)
{
    for (int i = 1; i < length; i++)
    {
        int temp = arr[i];
        int pos = i;
        for (int j = i - 1; j >= 0; j--)
        {
            if(arr[j] > temp)
            {
                arr[pos] = arr[j];
                pos = j;
            }

        }

        arr[pos] = temp;
    }
}

注意这里特地写了两种方法,第一种是直接调用C++提供的swap函数,需要进行内存的操作,而第二种则使用了我们前面所述的方法。虽然swap看上去代码可读性更好,但是因为效率的问题,方法1的运行时间会比方法2多出几倍。后面我会写专门的测试用例来验证这一点。

四、归并排序
算法复杂度nlogn
归并排序是比较经典的用空间换时间,是分治算法的一种典型运用,同时还用到了递归。归并算法很难用语言描述清楚,这里上两张图。
这里写图片描述
每次把数组进行划分,直到只有一个元素时数组就是有序的了,这时候递归返回,对Level2进行归并,这时候Level2就是有序的了,再返回,对Level1进行归并,以此类推。
归并的具体做法可以看下面这张图:
这里写图片描述
每次需要开辟一片新的内存来保存原来的数组数据,然后需要三个索引来进行操作,k每次的取值为x,y中较小的那个,同时需要考虑越界的问题,具体还是看代码来理解吧。

void insertionSort(int arr[], int l , int r)
{
    for(int i = l + 1; i <= r; i++)
    {
        int temp = arr[i];
        int pos = i;

        for(int j = i - 1; j >= l; j--)
        {
            if(arr[j] > temp)
            {
                arr[pos] = arr[j];
                pos = j;
            }
        }

        arr[pos] = temp;
    }
}

void doMerge(int arr[], int l, int mid, int r)
{
    int aux[r - l + 1];
    for(int i = l; i <= r; i++)
    {
        aux[i - l] = arr[i];
    }

    int x = l;
    int y = mid + 1;

    for(int k = l; k <= r; k++)
    {
        if(x > mid)
        {
            arr[k] = aux[y - l];
            y++;
        }
        else if(y > r)
        {
            arr[k] = aux[x - l];
            x++;
        }
        else if(aux[x - l] < aux[y - l])
        {
            arr[k] = aux[x - l];
            x++;
        }
        else
        {
            arr[k] = aux[y - l];
            y++;
        }
    }

}

//闭区间[l, r]
void realMergeSort(int arr[], int l, int r)
{
    if(r - l <= 15)
    {
        insertionSort(arr, l, r);
        return;
    }

    int mid = (l + r) / 2;
    realMergeSort(arr, l, mid);
    realMergeSort(arr, mid + 1, r);

    if(arr[mid] > arr[mid + 1])
    {
        doMerge(arr, l, mid, r);
    }
}

void mergeSort(int arr[], int length)
{
    realMergeSort(arr, 0, length - 1);
}

注意这里在代码里还加入了两点优化:
1、当归并排序的数据区间小于某个值时,我们可以使用插入排序来替代归并。
2、只有arr[mid] > arr[mid + 1]时,才需要进行归并。

五、快速排序
算法复杂度nlogn
快速排序的思想也是分治思想+递归的一种典型实现,标准的快速排序就是把数组分成大于v和小于v的两部分,v一般取数组的第一个元素,分组成功后分别对大于v和小于v的部分继续进行快速排序。可看图参考:
这里写图片描述
可见这里需要三个索引,其中j记录的就是v最终应该所在的位置。

int partition(int arr[], int l, int r)
{
    int v = arr[l];
    int j = l;
    for(int i = l + 1; i <= r; i++)
    {
        if(arr[i] < v)
        {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }

    swap(arr[l], arr[j]);

    return j;
}

void realQuickSort(int arr[], int l, int r)
{
    if(l >= r)
    {
        return;
    }

    int pos = partition(arr, l, r);

    realQuickSort(arr, l, pos - 1);
    realQuickSort(arr, pos + 1, r);
}

void quickSort(int arr[], int length)
{
    realQuickSort(arr, 0, length - 1);
}

标准的快排在面对基本有序的数组时性能会急剧下降,我们可以想见在上面的算法时其实我们对等于v的部分没有进行任何处理,所以对于基本有序的数组,这样的做法其实有大量的排序是做了无用功,这才有了更好的三路快速排序,上个图大概演示一下:
这里写图片描述
lt指向小于v的部分,gt指向大于v的部分,可见如果等于v的元素很多的时候,将大大减少算法的排序次数。

void realQuickSort3Ways(int arr[], int l, int r)
{
    if( r - l <= 15 )
    {
        insertionSort(arr, l, r);
        return;
    }

    swap( arr[l], arr[rand() % (r-l+1) + l]);

    int v = arr[l];
    int i = l + 1;
    int lt = l;
    int gt = r + 1;

    while(i < gt)
    {
        if(arr[i] > v)
        {
            swap(arr[i], arr[gt - 1]);
            gt--;
        }
        else if(arr[i] < v)
        {
            swap(arr[i], arr[lt + 1]);
            lt++;
            i++;
        }
        else
        {
            i++;
        }
    }

    swap(arr[l], arr[lt]);

    realQuickSort3Ways(arr, l, lt - 1);
    realQuickSort3Ways(arr, gt, r);
}

void quickSort3Ways(int arr[], int length)
{
    srand(time(NULL));
    realQuickSort3Ways(arr, 0, length - 1);
}

注意这里依然加入了两个优化,一个是前面提过的当数据量较小且基本有序时,插入排序是效率最高的,第二个是对数组进行一定的随机排序也可以提高快排的效率,这里涉及复杂的数学证明,就不赘述了。

基础排序的基本技巧就是控制好几个临界点,然后理解算法本身的执行流程,代码写起来才会行云流水。

先到这,这篇拖了好久了~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值