数据结构与算法读书笔记 - 005 - 基本排序和二分查找

void rank(int x[], int r[], const int size)
{
    for(int i = 1; i < size; ++i)
    {
        for(int j = 0; j < i; ++j)
        {
            if(x[j] <= x[i])
                ++r[i];
            else
                ++r[j];
        }
    }
}

int main(int argc, const char * argv[])
{
    const int size = 10;
    int array_toSort[size] = {1,6,4,3,5,7,8,5,4,3};
    int array_rank[size] = {0};
    rank(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_rank[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果:
0 7 3 1 5 8 9 6 4 2

再试一次;

//
//  main.cpp
//  SortAndOthers
//
//  Created by Bert Jiachen Wang on 1/17/21.
//

#include <iostream>
#include <string>

using std::cout; using std::endl;
using std::string;

//void rank(int x[], int r[], const int size)
//{
//    //initialize r
//    for(int i = 0; i < size; ++i)
//    {
//        r[i] = 0;
//    }
//
//    //这叫做比较所有数对
//    for(int i = 1; i < size; ++i)
//    {
//        for(int j = 0; j < i; ++j)
//        {
//            if(x[j] <= x[i])
//                ++r[i];
//            else
//                ++r[j];
//        }
//    }
//}

void rank(int x[], int r[], const int size)
{
    for(int i = 1; i < size; ++i)
    {
        for(int j = 0; j < i; ++j)
        {
            if(x[j] <= x[i])
                ++r[i];
            else
                ++r[j];
        }
    }
}

int main(int argc, const char * argv[])
{
    const int size = 5;
    int array_toSort[size] = {5,4,3,2,2};
    int array_rank[size] = {0};
    rank(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_rank[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果:
4 3 2 0 1
Program ended with exit code: 0
————————————————————
原理和注意事项:
原理出乎意料地不好理解。
为什么左加一下右加一下就得到正确的ranking了呢?

我比较喜欢分别从“比较过程”和从“结果”来想:

注意在比较过程中有一个步骤,是将所有的元素“两两比较”一下。如果一个元素比另外一个元素小,那对它没什么影响,如果一个元素比另一个元素大,它要加上。单独拿出一个元素来看,在“两两比较”的过程中,这个元素要和所有的其它元素比较一次,有多少比它小的,就要加上几次1。

而在结果中,如果把所有数据都排排好,然后给他们写上顺序,正是:每个元素,比它小的元素的个数,和它的排序成正相关。

结果和过程相对应->过程所做的可以得到正确的结果

关键词/关键步骤:
元素两两比较,看似是一个很小的步骤,但是却起到核心的作用。

需要注意的细节:
第一个循环, i是从=1开始的,从某种程度上,这是后面的j的行为决定的。(其实这里看的不是很明显,所以是从某种程度上。)不过要比较两个数,第二从0开始(j),那么第一个就要从1开始(i)

关键词/关键步骤:
对于for循环的问题,外层的行为某种程度上是由内层的行为决定的,虽然说知道了这一点对解决问题并没有什么用处。

————————————————————
选择排序:
我用的是swap。。。
不过我这个好像不是选择排序。。。无所谓了。。。
啊,这个叫做原地重排 ->

void rank_sort(int x[], int r[], const int size)
{
    for(int i = 0; i < size; ++i)
    {
        r[i] = 0;
    }
    
    for(int i = 1; i < size; ++i)
    {
        for(int j = 0; j < i; ++j)
        {
            if(x[j] <= x[i])
                ++r[i];
            else
                ++r[j];
        }
    }
    for(int i = 0; i < size; ++i)
    {
        while(r[i] != i)
        {
            int t = r[i];
            std::swap(x[i], x[t]);
            std::swap(r[i], r[t]);
        }
    }
}

使用

int main(int argc, const char * argv[])
{
    const int size = 5;
    int array_toSort[size] = {5,4,3,2,2};
    int array_rank[size] = {0};
    
    rank_sort(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果:
2 2 3 4 5
Program ended with exit code: 0
再试一次:

int main(int argc, const char * argv[])
{
    const int size = 10;
    int array_toSort[size] = {5,8,1,2,6,1,8,9,12,10};
    int array_rank[size] = {0};
    
    rank_sort(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;
    
    return 0;
}

1 1 2 5 6 8 8 9 10 12
Program ended with exit code: 0
————————————————————

————————————————————
好吧这才是正了八景的rank sort(需要一个附加数组的)

void rank_sort_2(int x[], int r[], const int size)
{
    for(int i = 0; i < size; ++i)
    {
        for(int j = 0; j < i; ++j)
        {
            if(x[j] <= x[i])
                ++r[i];
            else
                ++r[j];
        }
    }
    int* array_temp = new int[size];
    for(int i = 0; i < size; ++i)
    {
        array_temp[r[i]] = x[i];
    }
    for(int i = 0; i < size; ++i)
    {
        x[i] = array_temp[i];
    }
    delete[] array_temp;
}

使用:

int main(int argc, const char * argv[])
{
    const int size = 10;
    int array_toSort[size] = {5,8,1,2,6,1,8,9,12,10};
    int array_rank[size] = {0};
    
    rank_sort_2(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_rank[i] << " ";
    }
    cout << endl;
    
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果:
3 5 0 2 4 1 6 7 9 8
1 1 2 5 6 8 8 9 10 12
再试一次:

int main(int argc, const char * argv[])
{
    const int size = 12;
    int array_toSort[size] = {4,8,1,77,6,1,8,14,67,32,5,16};
    int array_rank[size] = {0};
    //rank(array_toSort, array_rank, size);
    //for(int i = 0; i < size; ++i)
    //{
    //    cout << array_rank[i] << " ";
    //}
    //cout << endl;
    
    rank_sort_2(array_toSort, array_rank, size);
    for(int i = 0; i < size; ++i)
    {
        cout << array_rank[i] << " ";
    }
    cout << endl;
    
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果:
2 5 0 11 4 1 6 7 10 9 3 8
1 1 4 5 6 8 8 14 16 32 67 77
Program ended with exit code: 0

总结起来一句话:
对于每一次for循环的i:((当然是从小到大)的顺序,或者是从大到小的顺序)
把i在数组x中对应位置的数字,放在i在数组r中对应位置的数字表示的新数组(temp)位置上。

关键词/关键动作
1 - 每一个循环的i对应两个数字,一是数据(x),二是位置(r)
2 - r中所存的数字的最大数字和size是对应的 = size - 1。这样才能正好在跑完r后将所有的temp数组的位置正好填满,没有重合,多余和遗漏。。。

————————————————————
选择排序 - selection sort

void selection_sort(int x[], const int size)
{
    for(int i = size - 1; i > 0; --i)
    {
        int indexOfMax = 0;
        for(int j = 0; j < i; ++j)
        {
            if(x[indexOfMax] < x[j+1])
                indexOfMax = j+1;
        }
        std::swap(x[indexOfMax], x[i]);
    }
}

使用

int main(int argc, const char * argv[])
{
    const int size = 12;
    int array_toSort[size] = {4,8,1,77,6,1,8,14,67,32,5,16};
    int array_rank[size] = {0};

    selection_sort(array_toSort, size);
    
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;
    
    return 0;
}

结果
1 1 4 5 6 8 8 14 16 32 67 77
Program ended with exit code: 0

没啥可说的
不过当时感觉选择最大值的那个方法还挺好玩的,给我一种一个球来回碰撞的感觉,谁大就传给谁,没有那个大就自己拿着

关键词:
返回最大值序号的函数;

int indexOfMax(int x[], const int size)
{
    int iOfMax = 0;
    for(int i = 1; i < size; ++i)
    {
        if(x[iOfMax] <= x[i])
            iOfMax = i;
    }
    return iOfMax;
}

使用:

int main(int argc, const char * argv[])
{
    const int size = 12;
    int array_toSort[size] = {4,8,1,77,6,1,8,14,67,32,5,16};
    int array_rank[size] = {0};

    cout << indexOfMax(array_toSort, size) << endl;

    return 0;
}

结果:
3
————————————————————
及时终止的选择排序:

void selection_sort_stop(int x[], const int size)
{
    bool sorted = false;
    for(int i = size - 1; !sorted && i > 0; --i)
    {
        sorted = true;
        int indexOfMax = 0;
        for(int j = 0; j < i; ++j)
        {
            if(x[indexOfMax] < x[j+1])
            {
                indexOfMax = j + 1;
                sorted = false;
            }
        }
        std::swap(x[i], x[indexOfMax]);
    }
}

没啥可说的,就是,看来给bool变量赋很多次没什么用处的值,比判断多次要划得来一些。。。也许判断就是很费时间呢。。。
————————————————————
冒泡排序:

void bubble_sort(int x[], const int size)
{
    for(int i = size - 1; i > 0; --i)
    {
        for(int j = 0; j < i; ++j)
        {
            if(x[j] > x[j+1])
                std::swap(x[j], x[j+1]);
        }
    }
}

使用

int main(int argc, const char * argv[])
{
    const int size = 12;
    int array_toSort[size] = {4,8,1,77,6,1,8,14,67,32,5,16};
    
    bubble_sort(array_toSort, size);
    
    for(int i = 0; i < size; ++i)
    {
        cout << array_toSort[i] << " ";
    }
    cout << endl;

    return 0;
}

结果
1 1 4 5 6 8 8 14 16 32 67 77
Program ended with exit code: 0

也没什么可说的。
1 - 至于外层的i在2(i > 1)还是停在1(i > 0),我想还是应该停在1,如果停在2,把0 - 2这几个数进行一次冒泡之后,仍有位置0大于位置1的可能。
书上的确有一个”i > 1“,但是要注意这个i,在内层变成“ n - 1",所以实质上应该还是停在1处,不过我也没有仔细推敲过,只是看起来像而已了
2 - 好玩的地方在于,对于这种算法,想是从左往右想的(的确有从右往左的成分),即第一次从左往右排一次,第二次从左往右排一次,第三次从左往右排一次。。。从右往左的成分就在于,每次排序的尽头在一个个向左移动,这体现在在外层的i,要从size - 1开始,然后一点点减小。
或者说,兴趣点在于,人类思考是先思考一次排序,然后考虑到尽头的问题,而写代码的时候,是现在外层套上一个个减小的尽头,然后再去写内层的一次次排序
————————————————————
及时终止的冒泡排序

void bubble_sort_stop(int x[], const int size)
{
    bool sorted = false;
    for(int i = size - 1; !sorted && i > 0; --i)
    {
        sorted = true;
        for(int j = 0; j < i; ++j)
        {
            if(x[j] > x[j+1])
            {
                std::swap(x[j], x[j+1]);
                sorted = false;
            }
        }
    }
}

在“及时终止的选择排序”中忘了提的,同时也是“及时终止的冒泡排序”中出现的,在第一层for循环中出现的,一起判断sorted和i是否到达边界。
我相信这里有一点书中算法书中没有提,但是C++primer中应该有提到过,即&&如果左侧的条件没有达到,那么右边的条件就不会去判断了。
所以我坚持把判断sorted写在左边,因为!sorted是一个更容易不满足的条件。如果它达到了,就不用去判断右边了
————————————————————
在有序数组中插入一个数字

//在有序数组中插入一个数字
void insertInSorted(vector<int> &sorted, int n)
{
    sorted.push_back(n);
    for(int i = (int)sorted.size() - 2; sorted[i] > sorted[i + 1] && i >= 0; --i)
    std::swap(sorted[i], sorted[i+1]);
}

在这里我用了vector,因为更方便与push_back
插入一个数字的时候一定要注意一定要判断这个数字一直到i = 0,因为很可能要和位于0位置上的数字也换一下。
另外,还是我自己的经验 - 一定要检验一下边界,比如插入一个最小的数字,插入一个最大的数字,等等
(for循环很大程度上考验的就是边界)
————————————————————
插入排序

void insertion_sort(int x[], const int size)
{
    for(int i = 1; i < size; ++i)
    {
        for(int j = i - 1; x[j] > x[j+1] && j >= 0; --j)
        std::swap(x[j], x[j+1]);
    }
}

插入排序就是把“之前”的一部分都当成是一个排好顺序的数组,然后将这一部分的后一位看能插入到其中的什么位置。
仍然是在for循环中做判断,一开始我还不是很适应在for循环中做判断,后来写了几次,呃仍然不是很适应,有好多次我都把判断写在外面了。。。
需要注意的是,书上并没有用swap,算法书上的用到swap的次数要比我用的少的多。其实对于int来说用swap并没有直接赋值更经济,反而要浪费更多的时间。
————————————————————
二分查找

int binary_search(const vector<int> &sorted, int n)
{
    int left = 0;
    int right = sorted.size() - 1;
    while(left <= right)
    {
        int middle = (left + right) / 2;
        if(sorted[middle] == n)
            return middle;
        else if(n > sorted[middle])
            left = middle + 1;
        else
            right = middle - 1;
    }
    return -1;
}

好吧我承认我写二分查找写的不够多,犯了好几个严重的错误(之前犯过的)而且最后还是照着书写出来的。写错的原因在于对于细节并不领悟(主旨早就知道了,但是并未完全理解细节)
首先left和right一定要等于middle 加1或减1,原因在于,如果left和right一直等于middle,这个程序永远都结束不了。
另外一个原因在于,middle既然小于或大于要查找的数,那么就和要查找的数就无关了,left和right应该跳过middle

边界条件仍然至关重要。二分法的边界条件在于可能有计数偶数两种情况。其实完全不用害怕,只不过两种情况而已
即使不是边界也是两种情况,如果是left加right是奇数,那么middle就是他们中间的那一个,如果是偶数,就是中间偏左的那个数。
到了边界的情况,如果剩下两个数,那么middle就是left,如果left/middle不是n,left的+1可以是判断挪到right,去判断是不是n
如果只剩下一个数,left和right是一样的,此时还要继续(满足while的条件,这也是为什么while的条件是left <= right),这时进行运算后middle也是这个数,left, 或right, 或middle, 判断这个middle是不是n。
另外,一定要注意不是middle是否等于n,二是数组的middle位是否等于n。
其实我也不是特别理解这么做的好与坏,我直接就读了算法的实现,并没有考虑过任何其它种情况。
————————————————————
参考 / 读书笔记读的书:
————————————————————

C++ Primer(第五版)ISBN 978-7-121-15535-2
数据结构,算法与应用:C++语言描述(第二版)ISBN 978-7-111-49600-7

更新于
2021.01.17

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值