Algorithm Review 1 基础排序算法

来源:互联网 发布:dnf阿里云搭建 编辑:程序博客网 时间:2024/05/01 04:21

整天做上层架构设计和写界面,把计算机最重要的算法与数据结构都忘得差不多了。所以从这篇开始系统地复习常见的算法与数据结构,这里会暂时抛弃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 0
原创粉丝点击