C++数据结构之数组详解

主要参考01.数组基础知识 | 算法通关手册 (itcharge.cn)

上文算法通关手册已经讲的非常清楚了,甚至把C++,java,python的异同也含盖进去了

以下是我的总结


1.C++数组基础

1.1 数组的定义

数组(Array):一种线性表数据结构。它使用一组连续的内存空间,来存储一组具有相同类型的数据。

上述链接也提到了,python的list已经不能算是严格的数组了,它的list结构可以存储不同类型的数据。

逻辑结构:线性表。

存储结构:使用一组连续的内存空间。

1.2 数据运算

查找元素

  • 根据索引访问元素:时间复杂度O(1)

  • 根据值去查找位置:时间复杂度O(n)

插入元素

  • 尾部插入元素:时间复杂度O(1),python直接用append封装了该方法

  • 数组第i处插入元素:最坏和平均时间复杂度都是 O(n)

修改元素

时间复杂度O(1)

删除元素

  • 删除尾部元素:时间复杂度为 O(1)

  • 删除第i个位置的元素:最坏和平均时间复杂度都是 O(n)

  • 基于条件的删除:时间复杂度为 O(n),可以通过算法改进,让删除多个元素导致的移位操作一次就行

数组总结

数组的最大特点的支持随机访问。其访问元素、改变元素的时间复杂度为 O(1),在尾部插入、删除元素的时间复杂度也是 O(1),普通情况下插入、删除元素的时间复杂度为 O(n)。

2.C++数组进阶

2.1 字符串数组的拼接问题

C++自带的数组如下

#include
#include
using namespace std;

int main(void){
	
    char *a = "abcde";
    char a1[] = "abcde";
    char a2[10] = "abcde";
    int b[10] = {1,2,3,4};
    int c[] = {1,2,3,4};
    cout << sizeof(a) <<endl;     // 4, 指针一直是4个字节 
    cout << sizeof(a1) <<endl;    // 6, 这里包含结束字符'\0',strlen函数不包含 
    cout << sizeof(a2) <<endl;    // 10,一开始定义好的,单个字符占一个字节 
    cout << sizeof(b) <<endl;     // 40,整形占4个字节 
    cout << sizeof(c) <<endl;     // 16 
    return 0;
}

这里要注意的是,

  • 单个字符占1个字节,但如果是字符常量,则占4个字节。

  • sizeof计算字符串长度时,会计算结束符。

通过上面的初始化方式,我们可以发现内存空间已经被分配好了,如果调用拼接函数去拼接已经没有内存的字符数组时,可能无法得到想要的结果。

//错误示范
strcat(a1, "nihao");
cout << a1 <<endl;
//正确示范
char* aa = new(char[20]);
strcpy(aa, a);
strcat(aa, "nihao");
cout << aa <<endl;

虽然上述错误示范也可能会成功,但是它可能错误地改变了程序中其他部分的内存数据,有可能不会抛出异常,但会导致程序数据错误,也可能由于非法内存访问抛出异常。

如果你采用的vs studio进行编程,可以尝试使用strcat_s函数进行拼接,这样更加的规范,strcat_s是属于微软提供的一个类库中的函数,dev是不支持的。

2.2 vector容器动态扩容

可以说vector好比C语言中的数组,但是又具有数组没有的高级功能,不需要在初始化的时候制定数组的大小,能够动态扩容。

vector的底层实际就是一段连续的空间,和比较重要的是采用了三个指针来实现对vector的数据操作。

vector扩大容量的本质

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放

扩容会耗费大量时间,建议一开始设置好数组大小

vector数据操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrhjtqHF-1662298710467)(image-20220829105443267.png)]

取自C++的参考文档

采用指针操作可以防止数组越界,所以尽量指针进行操作,不过同样也会出现一定的问题,比如insert和erase函数,当使用他们后,原先的迭代器就没用了,就会出现野指针。

解决方法:保持插入和删除的值是下一个值。

vector<int> v;
v.Push_back(1);
v.Push_back(2);
v.Push_back(3);
v.Push_back(4);	
vector<int>::iterator it = v.begin();
it = v.insert(it+1 , 9);
cout << *it << " ";
cout << endl;

主要参考博客

(3条消息) 面试官都在问 | 请谈谈vector的底层实现_月已满西楼的博客-CSDN博客_vector的底层实现

(3条消息) Vector容器的底层实现_江南伯爵.的博客-CSDN博客_vector底层实现

C++ vector(STL vector)底层实现机制(通俗易懂) (biancheng.net)

许多问题需要在实际应用才能领悟更加深刻,总之vector容器相比于数组,其使用“性价比”是非常高的。

2.3 数组排序

排序永远是一个经典的话题,这里介绍10种基础排序,具体情况在上面链接有详细的介绍。

冒泡排序

  • 算法思想:每次比较相邻元素之间的大小,使值小的逐步从后面移动到前面,值大的从前面移到后面。
  • 时间复杂度:
    • 最好时间复杂度:O(n)
    • 最坏时间复杂度:O(n2)
  • 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变值相同元素的相对位置,因此,冒泡排序法是一种 稳定排序算法

C++实现

vector<int>& BubbleSort(vector<int> &arr) {
    //执行n-1次 
    for (auto i = 0; i < arr.size()-1; i++) {
        //比较前n-i-1次元素
        for (auto j = 0; j < arr.size() - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
};

经典的两重for循环,执行n-1次,每次对前n-i+1个元素进行两两比较。

选择排序

  • 算法思想:每次从未排序的元素中找到最小的值,并与未排序最前面的元素交换位置。
  • 时间复杂度:因为排序比较次数与序列的原始状态无关,总是O(n2)
  • 排序稳定性:因为交换不是相邻两元素,可能会改变值相同元素的前后位置,故是一种不稳定的排序算法

感觉这个算法很拉,不过还是有优点,那就是不需要开辟额外的空间存储变量。

C++实现

vector<int>& SelectionSort(vector<int> &arr) {

    for (auto i = 0; i < arr.size() - 1; i++) {
        int min_i = i;
        //全局搜索最小值
        for (auto j = i+1; j < arr.size(); j++) {
            if (arr[j] < arr[min_i]) {
                min_i = j;
            }
        }
		//交换
        if (i != min_i) {
            int temp = arr[i];
            arr[i] = arr[min_i];
            arr[min_i] = temp;
        }
    }
    return arr;
};

同样是两重for循环,但是选择排序将元素的交换放在了最后,而不是每次都进行元素交换,速度比冒泡排序快了很多,详情看下方的测试结果。

插入排序

  • 算法思想:每次将无序列表中的元素插入到有序列表中。
  • 时间复杂度:
    • 最佳时间复杂度:O(n)
    • 最差时间复杂度:O(n2)
    • 平均时间复杂度:O(n2)
  • 排序稳定性:稳定排序算法

C++实现

vector<int>& InsertionSort(vector<int> &arr) {

    for (auto i = 0; i < arr.size(); i++) {
        int temp = arr[i];
        int j = i;
        //比较插入值与顺序表中的值的大小,如果小则顺序表中的元素向右移动
        while(j>0 && arr[j-1] > temp){
            arr[j] = arr[j - 1];
            j -= 1;
        }
        //将插入值插入顺序表
        arr[j] = temp; 
    }
    return arr;
};

这里我们发现插入排序的效果在随机数据下又更加的迅速,这里我的理解是,他除了和选择算法一样,把最后的“交换”步骤放在了最后一步,且因为前排序列是有序的,所以一旦找到了插入的位置,就可以停止运算。

巧妙在于,选择算法是在无序列表中寻找最大值,而插入算法是在有序列表中寻找最优位置,且发挥了有序列表不用遍历所有值的特点。

当然这些速度的差异都是一个量级的,在超大规模问题上,就显得不够快了。下面我们来看一下更快的排序是什么样子的。

希尔排序

  • 算法思想:将整个序列按照一定的间隔划分,每个子序列分别进行插入排序,并逐渐缩小间隔,直到排序间隔为1。
  • 时间复杂度:介于O(nlog2n) 和O(n2)之间。
  • 排序稳定性:是一种无稳定的排序算法。

C++实现

vector<int>& ShellSort(vector<int> &arr) {

    int gap = arr.size() / 2; //向下取整

    while (gap > 0) {
        //对每个元素进行插入排序
        for (int i = gap; i < arr.size(); i++) {

            int temp = arr[i];
            int j = i;
            while (j >= gap && arr[j - gap] > temp) {
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = temp;
        }
		//重新分配间隔
        gap /= 2;
    }
    return arr;
};

里面明明嵌套了三重循环,速度却快的令人发指!,和之前三个排序算法不是一个量级。这里我的理解是,首先gap的值每次变化是"/2",所以第一层while循环为log(n)数量级,内层for循环则是n的数量级。

而最后的for循环,首先随着子序列分的越多,for循环的次数就越少,而当子序列不断扩大时,整个序列也逐步有序,for循环的次数也不会随之增加。我们知道如果for循环中的循环次数为常数时,时间复杂度是O(1)的,但是由于这里的for循环次数不好计算,但是又不会随子序列的扩大而增加,所以这里介于O(nlog(n)) 和O(n2)之间。

归并排序

  • 算法思想:每次比较相邻元素之间的大小,使值小的逐步从后面移动到前面,值大的从前面移到后面。
  • 时间复杂度:
    • 最好时间复杂度:O(n)
    • 最坏时间复杂度:O(n2)
  • 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变值相同元素的相对位置,因此,冒泡排序法是一种 稳定排序算法

C++实现

vector<int> Merge(vector<int> left_arr, vector<int> right_arr) {

    vector<int> arr;
    int left_i,right_i;
    left_i = right_i = 0;
	//分离双指针进行数组合并
    while (left_i < left_arr.size() && right_i < right_arr.size()) {
        //将两个有序子序列中较小元素依次插入到结果数组中
        if (left_arr[left_i] < right_arr[right_i]) {
            arr.push_back(left_arr[left_i]);
            left_i++;
        }
        else {
            arr.push_back(right_arr[right_i]);
            right_i++;
        }
    }
	//左边数组剩余值
    while (left_i < left_arr.size()) {
        arr.push_back(left_arr[left_i]);
        left_i++;
    }
	//右边数组剩余值
    while (right_i < right_arr.size()) {
        arr.push_back(right_arr[right_i]);
        right_i++;
    }
    return arr;
}
vector<int> MergeSort(vector<int> arr) {

    if (arr.size() <= 1) {
        return arr;
    }
    int mid = arr.size() / 2;
    //得到左边经过排序好的数组
    vector<int> left_arr = this->MergeSort(vector<int>(arr.begin(), arr.begin() + mid));
    //得到右边经过排序好的数组
    vector<int> right_arr = this->MergeSort(vector<int>(arr.begin() + mid, arr.end()));
    //将两个有序的数组进行合并
    return this->Merge(left_arr,right_arr);
};

因为这里的归并排序采用递归实现,所以这里没有采用引用方式传参和返回。

最后总体来说,归并排序的内存占用还是较大的。

快速排序

  • 算法思想:随机取序列中的一个值,并将序列以此值为界限,一分为二,并递归进行,直到整个序列有序
  • 时间复杂度:
    • 最好时间复杂度:O(nlog2n)
    • 最坏时间复杂度:O(n2)
    • 平均之间复杂度:O(nlog2n)
  • 排序稳定性:不稳定排序算法

C++实现

int RandomPartition(vector<int> &arr, int low, int high) {
    srand((int)time(0));
    int res = rand() % (high - low) + low;
    int temp = arr[low];
    arr[low] = arr[res];
    arr[res] = temp;
    return this->Partition(arr, low, high);
}
int Partition(vector<int> &arr, int low, int high) {
    int pivot = arr[low];
    int i = low + 1;
    for (int j = i; j < high + 1; j++) {
        if (arr[j] < pivot) {
            int temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
            i++;
        }
    }
    int temp = arr[i - 1];
    arr[i - 1] = arr[low];
    arr[low] = temp;
    返回正确基准节点的位置
    //cout << "pi:" << i - 1 << "\t" << pivot << endl;
    //for (auto i = 0; i < arr.size(); i++) cout << arr[i] << "\t"; cout << endl;
    return i - 1;
}
vector<int> QuickSort(vector<int> &arr,int low,int high) {
    if (low < high) {
        //生成随机数,并以该值进行一次划分
        int pi = this->RandomPartition(arr, low, high); 
		//将左边的数组再次进行快排
        this->QuickSort(arr, low, pi - 1);
        //将右边的数组再次进行快排
        this->QuickSort(arr, pi + 1, high);
    }
    return arr;
}

这里的快速排序采用递归的方式实现,测试时,当数据量大于10e5时,出现了栈溢出,网上搜索过后,这种情况确实是存在的,同样也有着非递归实现,以及快排的进一步优化,这个我们放在算法里面在进行讲解,而这里主要讲的是数组这种数据结构。

这里为什么归并没有出现栈溢出,原因是归并的每次划分都是平均了,所以递归下去稳定是log2n数量级,而归并可能由于随机值选取的问题导致递归深度更加深,最糟糕的情况就是数量级n的情况。

QuickSort中的arr参数一定要是引用传递或者指针,不然可能返回的结果不变

堆排序

  • 算法思想:建立在堆上的选择排序

    堆:完全二叉树

    大顶堆:根节点值≥子节点值

    小顶堆:根节点值≤子节点值

  • 时间复杂度:O(nlog2n)

  • 空间复杂度:O(n)

  • 排序稳定性:不稳定排序算法

C++实现

void sift_down(vector<int> &arr, int start, int end) {
    //计算父节点和子节点的下标
    int parent = start;
    int child = parent * 2 + 1;
    while (child <= end) {
        //先比较两个子结点大小,选择最大的
        if (child+1 <= end && arr[child] < arr[child + 1]) child++;
        //如果父节点比子结点大,代表调整完毕
        if (arr[parent] >= arr[child])
            return;
        else {
            swap(arr[parent], arr[child]);
            parent = child;
            child = parent * 2 + 1;
        }
    }
}
vector<int> HeapSort(vector<int> &arr, int len) {
    //从最后一个节点的父节点开始,sift_down以完成堆化
    for (int i = (len - 1 - 1) / 2; i >= 0; i--) sift_down(arr, i, len - 1);
    //先将第一个元素和已经排好的元素前一位做交换,在重新调整,直到排序完毕
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        sift_down(arr, 0, i - 1);
    }
    return arr;
}

堆排序的时间复杂度主要体现在构建堆和调整堆,首先调整堆,如果要将根元素调整到叶子节点,最多需要深度d-1次比较交换即可,而调整到d-1层则需要d-2次,因为是完全二叉树,最坏的情况初始化所有堆区,通过求和公式得到的结果为2n。而取出最大元素,在进行调整最坏情况为log2n数量级,所以综合时间复杂度为O(nlog2n)

这里只是通俗的理解,最上方链接有详细的讲解和求和公式的计算。

计数排序

  • 算法思想:使用一个范围涵盖所有数据的额外数组统计原数组中元素出现的个数,在排到正确的顺序
  • 时间复杂度:O(n+k)
  • 空间复杂度:O(k)
  • 排序稳定性:稳定排序算法

C++实现

vector<int> CountingSort(vector<int> &arr) {

    auto maxPosition = max_element(arr.begin(), arr.end());
    auto minPosition = min_element(arr.begin(), arr.end());
    int size = *maxPosition - *minPosition + 1;
    vector<int> counts(size, 0);
	//计数
    for (auto i = 0; i < arr.size(); i++) counts[arr[i] - *minPosition]++;

    //cout << "对应的值" << endl;
    //for (auto i = *minPosition; i < *maxPosition+1; i++) cout << i << "\t"; cout << endl;
    //for (auto i = 0; i < counts.size(); i++) cout << counts[i] << "\t"; cout << endl;

    //计算排名
    for (int j = 1; j < size; j++)  counts[j] += counts[j - 1];

    //cout << "计算排名" << endl;
    //for (auto i = 0; i < counts.size(); i++) cout << counts[i] << "\t"; cout << endl;

    //通过排名逆向还原数组
    vector<int> res(arr.size(), 0);
    for (auto i = 0; i < arr.size(); i++) {
        //排名就是该值在arr中的索引
        res[counts[arr[i] - *minPosition] - 1] = arr[i];
        counts[arr[i] - *minPosition] -= 1;
    }

    return res;
}

这里计数排序的速度远大于去前面几种,是因为我的随机数的范围是1-10,所以时间复杂度中的k就无法体现,这也说明如果范围不大的话,计数排序是非常快的。

桶排序

  • 算法思想:
  • 时间复杂度:
    • 平均时间复杂度:O(n+n2 / m+m2)
    • 最坏时间复杂度:O(n2)
  • 空间复杂度:O(n+m)
  • 排序稳定性:稳定排序算法

C++实现

vector<int> BucketSort(vector<int> arr,int bucket_size = 2) {
    //计算最大元素,最小元素
    auto arr_max = max_element(arr.begin(), arr.end());
    auto arr_min = min_element(arr.begin(), arr.end());
    //定义桶的个数为
    const int bucket_count = (*arr_max - *arr_min) / bucket_size +1;

    //分桶存储元素
    vector<int>* buckets = new vector<int>[bucket_count];  //这里要申请到堆区,才能申请动态数组
    for (auto i = 0; i < arr.size(); i++) {
        buckets[(arr[i] - *arr_min) / bucket_size].push_back(arr[i]);
    }

    //桶内进行归并排序,或插入排序
    vector<int> res(arr.size());
    int p = 0;
    for (auto i = 0; i < bucket_count; i++) {
        //this->InsertionSort(buckets[i]);
        vector<int> arr_temp = this->MergeSort(buckets[i]);
        buckets[i].assign(arr_temp.begin(), arr_temp.end());
        
        //合桶
        for (int j = 0; j < buckets[i].size(); ++j) {
            res[p++] = buckets[i][j];
        }
    }

    delete[] buckets;
    return res;
}

这里经过分桶操作后,bucket_size设置为2,排序确实快了一些,但是我觉得没有完全发挥出桶的速度,因为这里的int范围在1-10,分桶数目有限,所以我这里单独准备做一组桶排序和计数排序的比较。

  • 设置bucket_size为1
  • 注释掉排序算法,因为这里每个桶只能是一个元素
数范围 10e3 10e4 10e6
1-10 3ms | 2ms 24ms | 20ms 2.024s |1.813s
1-10e3 6ms | 10ms 27ms | 37ms 1.945s |1.772s
1-32767 22ms |63ms(9ms) 39ms |105ms 2.172s |2.552s

因为rand函数的最大值为32767,所以作为最后的测试范围,分桶操作就和计数排序有相似之处,但是分桶操作不仅仅能计算整数排序,还可以计算浮点型,字符等,应用范围更广。

当然,除上面所讲之外,桶排序也有它的优势所在,如果一组数据的最小值最大值差非常大,但是数据量不大,采用计数排序时间会比较长,而桶排序可以通过调节控制桶的数量,来提升速度。

如上表,当随机数取值范围为1-32767时,我将桶的尺寸调整成20,那么速度就会拉满,只有9ms。

有兴趣的同学可以继续做更多的实现,测试代码在最后附录处。

基数排序

  • 算法思想:将整数按位数切割成不同的数字,然后按每个位数分别比较进行排序
  • 时间复杂度:O(n*k),k表示位数
  • 空间复杂度:O(n+k)
  • 排序稳定性:稳定排序算法

C++实现

vector<int> RadixSort(vector<int> &arr) {

    auto arr_max = max_element(arr.begin(), arr.end());
    int size = to_string(*arr_max).size();

    //对每一位进行一次比较
    for (auto i = 0; i < size; i++) {
		
        //0-9一共10个桶,获取数据
        vector<int> buckets[10];
        for (int j = 0; j < arr.size(); j++) {
            //cout << "res:" << arr[j] / int(pow(10, i)) % 10 << endl;
            buckets[arr[j] / int(pow(10, i)) % 10].push_back(arr[j]);
        }

        //重排数组
        arr.clear();
        for (int j = 0; j < 10; j++) {
            for (int k = 0; k < buckets[j].size(); k++) arr.push_back(buckets[j][k]);
        }
    }

    return arr;
}

基数排序的时间复杂度还是很低的,毕竟数字位数在多,也很难超过100的量级,所以基数排序是一个非常“稳健”的排序,他不像计数排序和桶排序遇到分配不均的情况效率较低,而是保持一个稳定的效率,因为分桶只有10个。

sort排序

学了这么多排序之后,那么C++自带的algorithm库中的排序是采用什么算法呢。

其实STL中的sort()并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。

//基本用法
sort(arr.begin(),arr.end());
//自定义准则
bool cmp(const int& a, const int& b)
{
	return a > b; //从大到小排序
}
sort(arr.begin(), arr.end(), cmp);   //这个定义可以是结构体
//匿名函数排序
sort(arr.begin(), arr.end(), [](const int &a, const int &b) {
    return a > b;
});

标准库中的sort的速度大多数情况下是比手写算法要快的。

听过一个问题,C++中五乘七怎么样优化最快,有各种各样的回答,其中一个是这样的。

5*7,因为如果你有办法优化的更快,那么5*7的底层为什么不能是你的优化方法呢。

同样的,如果你的排序在任何时候都比STL的sort要快,那么STL的sort实现就会采用你的方法,除非你是专门研究这的,不然用好STL就很好了。

测试结果

这里不同电脑是不一致,且生成随机值的范围为1-10,有一定的局限性。

排序方法 10 100 1000 10000 10e6
冒泡排序 1ms 5ms 457ms 43.441s -
选择排序 1ms 3ms 248ms 23.596s -
插入排序 0ms 2ms 143ms 12.797s -
希尔排序 0ms 2ms 7ms 117ms 14.117s
归并排序 1ms 4ms 54ms 515ms 57.363s
快排排序 0ms 2ms 30ms 1.19s -
堆排序 0ms 1ms 12ms 132ms 18.91s
计数排序 0ms 1ms 3ms 24ms 2.024s
桶排序(插) 3ms 6ms 24ms 1.375s -
桶排序(归) 2ms 9ms 56ms 484ms 54.7s
基数排序 0ms 2ms 9ms 29ms 2.206s
StlSort 0ms 0ms 1ms 3ms 363ms

最后代码如下,可以自己修改,测试。

排序代码

#include
#include
#include
#include
#include
#include
#define DEBUG

//调控
#define TIME
//#define OUTPUT

#ifdef DEBUG
#include
#endif
using namespace std;


class Solution {

public:
	vector<int>& BubbleSort(vector<int> &arr) {
		//执行n-1次 
		for (auto i = 0; i < arr.size()-1; i++) {
			for (auto j = 0; j < arr.size() - i - 1; j++) {
				if (arr[j] > arr[j + 1]) {
					int temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}

		return arr;
	};
	vector<int>& SelectionSort(vector<int> &arr) {
		
		for (auto i = 0; i < arr.size() - 1; i++) {
			int min_i = i;
			for (auto j = i+1; j < arr.size(); j++) {
				if (arr[j] < arr[min_i]) {
					min_i = j;
				}
			}

			if (i != min_i) {
				int temp = arr[i];
				arr[i] = arr[min_i];
				arr[min_i] = temp;
			}
		}
		return arr;
	};
	vector<int>& InsertionSort(vector<int> &arr) {

		for (auto i = 0; i < arr.size(); i++) {
			int temp = arr[i];
			int j = i;
			while(j>0 && arr[j-1] > temp){
				
				arr[j] = arr[j - 1];
				j -= 1;

			}
			arr[j] = temp;  //最开始时,arr[0]作为有序列表第一个值
		}
		return arr;
	};
	vector<int>& ShellSort(vector<int> &arr) {

		int gap = arr.size() / 2; //向下取整

		while (gap > 0) {
			//对每个元素进行插入排序
			for (int i = gap; i < arr.size(); i++) {

				int temp = arr[i];
				int j = i;

				while (j >= gap && arr[j - gap] > temp) {

					arr[j] = arr[j - gap];
					j -= gap;
				}

				arr[j] = temp;
			}

			gap /= 2;
		}
		return arr;
	};
	vector<int> MergeSort(vector<int> arr) {

		if (arr.size() <= 1) {

			return arr;
		}

		int mid = arr.size() / 2;
		vector<int> left_arr = this->MergeSort(vector<int>(arr.begin(), arr.begin() + mid));
		vector<int> right_arr = this->MergeSort(vector<int>(arr.begin() + mid, arr.end()));

		return this->Merge(left_arr,right_arr);
	};
	vector<int> QuickSort(vector<int> &arr,int low,int high) {
		if (low < high) {
			int pi = this->RandomPartition(arr, low, high);

			this->QuickSort(arr, low, pi - 1);   //此时的pi的值已经移动到了中间
			this->QuickSort(arr, pi + 1, high);
		}
		return arr;
	}
	vector<int> HeapSort(vector<int> &arr, int len) {
		//从最后一个节点的父节点开始,sift_down以完成堆化
		for (int i = (len - 1 - 1) / 2; i >= 0; i--) sift_down(arr, i, len - 1);
		//先将第一个元素和已经排好的元素前一位做交换,在重新调整,直到排序完毕
		for (int i = len - 1; i > 0; i--) {
			swap(arr[0], arr[i]);
			sift_down(arr, 0, i - 1);
		}
		return arr;
	}
	vector<int> CountingSort(vector<int> &arr) {

		auto arr_max = max_element(arr.begin(), arr.end());
		auto arr_min = min_element(arr.begin(), arr.end());
		int size = *arr_max - *arr_min + 1;
		vector<int> counts(size, 0);

		for (auto i = 0; i < arr.size(); i++) counts[arr[i] - *arr_min]++;
		for (int j = 1; j < size; j++)  counts[j] += counts[j - 1];
		vector<int> res(arr.size(), 0);
		for (auto i = 0; i < arr.size(); i++) {
			
			//这里一共有
			res[counts[arr[i] - *arr_min] - 1] = arr[i];
			counts[arr[i] - *arr_min] -= 1;
		}

		return res;
	}
	vector<int> BucketSort(vector<int> arr,int bucket_size = 2) {
		//计算最大元素,最小元素
		auto arr_max = max_element(arr.begin(), arr.end());
		auto arr_min = min_element(arr.begin(), arr.end());
		//定义桶的个数为
		const int bucket_count = (*arr_max - *arr_min) / bucket_size +1;

		vector<int>* buckets = new vector<int>[bucket_count];  //这里要申请到堆区,才能申请动态数组
		for (auto i = 0; i < arr.size(); i++) {
			buckets[(arr[i] - *arr_min) / bucket_size].push_back(arr[i]);
		}

		vector<int> res(arr.size());
		int p = 0;
		for (auto i = 0; i < bucket_count; i++) {
			//this->InsertionSort(buckets[i]);
			//vector arr_temp = this->MergeSort(buckets[i]);
			//buckets[i].assign(arr_temp.begin(), arr_temp.end());
			for (int j = 0; j < buckets[i].size(); ++j) {
				res[p++] = buckets[i][j];
			}
		}
		
		delete[] buckets;
		return res;
	}
	vector<int> RadixSort(vector<int> &arr) {

		auto arr_max = max_element(arr.begin(), arr.end());
		int size = to_string(*arr_max).size();
		
		for (auto i = 0; i < size; i++) {
			
			vector<int> buckets[10];
			for (int j = 0; j < arr.size(); j++) {
				//cout << "res:" << arr[j] / int(pow(10, i)) % 10 << endl;
				buckets[arr[j] / int(pow(10, i)) % 10].push_back(arr[j]);
			}

			arr.clear();
			for (int j = 0; j < 10; j++) {
				for (int k = 0; k < buckets[j].size(); k++) arr.push_back(buckets[j][k]);
			}
		}

		return arr;
	}
	vector<int> StlSort(vector<int> &arr){

		sort(arr.begin(), arr.end());
		return arr;
	}


private:
	vector<int> Merge(vector<int> left_arr, vector<int> right_arr) {

		vector<int> arr;
		int left_i,right_i;
		left_i = right_i = 0;

		while (left_i < left_arr.size() && right_i < right_arr.size()) {
			//将两个有序子序列中较小元素依次插入到结果数组中
			if (left_arr[left_i] < right_arr[right_i]) {
				arr.push_back(left_arr[left_i]);
				left_i++;
			}
			else {
				arr.push_back(right_arr[right_i]);
				right_i++;
			}

		}

		while (left_i < left_arr.size()) {
			arr.push_back(left_arr[left_i]);
			left_i++;
		}

		while (right_i < right_arr.size()) {
			arr.push_back(right_arr[right_i]);
			right_i++;
		}
		return arr;
	}
	int RandomPartition(vector<int> &arr, int low, int high) {
		srand((int)time(0));
		int res = rand() % (high - low) + low;
		int temp = arr[low];
		arr[low] = arr[res];
		arr[res] = temp;
		return this->Partition(arr, low, high);
	}
	int Partition(vector<int> &arr, int low, int high) {
		int pivot = arr[low];
		int i = low + 1;

		for (int j = i; j < high + 1; j++) {
			if (arr[j] < pivot) {
				int temp = arr[j];
				arr[j] = arr[i];
				arr[i] = temp;
				i++;
			}
		}

		int temp = arr[i - 1];
		arr[i - 1] = arr[low];
		arr[low] = temp;

		return i - 1;
	}
	void sift_down(vector<int> &arr, int start, int end) {
		//计算父节点和子节点的下标
		int parent = start;
		int child = parent * 2 + 1;
		while (child <= end) {
			//先比较两个子结点大小,选择最大的
			if (child+1 <= end && arr[child] < arr[child + 1]) child++;
			//如果父节点比子结点大,代表调整完毕
			if (arr[parent] >= arr[child])
				return;
			else {
				swap(arr[parent], arr[child]);
				parent = child;
				child = parent * 2 + 1;
			}
		}
	}

};

#ifdef DEBUG
struct Range {
	int low;
	int high;
	Range(int a, int b) {
		low = a; high = b;
	}
};

class Test {

public:
	vector<int> RandomInt(int n, Range &range) {
		srand((int)time(0));
		vector<int> arr(n);
		for (auto it = arr.begin(); it != arr.end(); it++) {
			*it =  rand()%(range.high-range.low)+range.low;
		}
		return arr;
	}
	void SortIsRight(const vector<int> &arr) {

		for (auto i = 0; i < arr.size() - 1; i++) {
			if (arr[i] > arr[i+1]) {
				cout << "排序失败" << endl;
				return;
			}
		}
		cout << "排序成功" << endl;
	}


};
#endif

int main() {

	Solution s;

#ifdef DEBUG
	Test t;
	Range range(1, 10);
	vector<int> test = t.RandomInt(1000000, range);
#ifdef OUTPUT
	for (auto it = test.begin(); it != test.end(); it++) {
		cout << *it << "\t";
	}
	cout << endl;
#endif
#ifdef TIME
	clock_t start = clock();
#endif
#endif

	//vector ans = s.BubbleSort(test);
	//vector ans = s.SelectionSort(test);
	//vector ans = s.InsertionSort(test);
	//vector ans = s.ShellSort(test);
	//vector ans = s.MergeSort(test);
	//vector ans = s.QuickSort(test,0, test.size()-1);
	//vector ans = s.HeapSort(test, test.size());
	vector<int> ans = s.CountingSort(test);
	//vector ans = s.BucketSort(test, 20);
	//vector ans = s.RadixSort(test);
	//vector ans = s.StlSort(test);

#ifdef DEBUG
#ifdef TIME
	clock_t finish = clock();
	cout << endl << "the time cost is:" << double(finish - start) / CLOCKS_PER_SEC << "s" << endl;
#endif
#ifdef OUTPUT
	for (auto it = ans.begin(); it != ans.end(); it++) {
		cout << *it << "\t";
	}
	cout << endl;
#endif
	t.SortIsRight(ans);
#endif

	return 0;
}

2.4 二分查找

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

这里提供两种方法

方法一:

  • target在一个左闭右闭的区间,[left,right]因此left和right是可以相等的。
  • 因为是闭区间,所以赋值都是mid-1或者mid+1
int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
    while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
        int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
        if (nums[middle] > target) {
            right = middle - 1; // target 在左区间,所以[left, middle - 1]
        } else if (nums[middle] < target) {
            left = middle + 1; // target 在右区间,所以[middle + 1, right]
        } else { // nums[middle] == target
            return middle; // 数组中找到目标值,直接返回下标
        }
    }
    // 未找到目标值
    return -1;
}

方法二:

  • target在一个左闭右开的区间,[left,right)。
  • 因为右边是开区间,所以当mid>target时,right=mid
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

2.5 C++数组之双指针

此指针非彼指针,这里的指针不是指的C++中的指针,而是索引,只要能够访问数组即可。

  • 对撞指针:两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题。
  • 快慢指针:方向相同,用于解决数组移动,删除元素以及是否有环,长度等问题。
  • 分离双指针:两个指针分别属于不同数组,例如排序算法中的归并排序就利用分离双指针进行了数组合并操作。

2.6 C++数组之滑动窗口

滑动窗口首先想到的是计算机网络中的滑动窗口协议。这里数组滑动窗口在给定数组/字符串上维护一个固定长度或不定长的窗口,可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。

  • 滑动操作:窗口按照一定的方向进行移动。
  • 缩放操作:对于不定长的窗口,可以从左侧缩小窗口长度,也可以右侧增大窗口长度。

解决问题

滑动窗口算法一般用来解决一些查找满足一定条件的连续区间的性质的问题,可以将一部分问题中的嵌套循环转变为一个单循环,因此他可以减少时间复杂度。

  • 固定长度窗口:窗口大小是固定的
  • 不定长度窗口:窗口大小不失固定的。
    • 求解最大的满足条件的窗口
    • 求解最小的满足条件的窗口

总结

关于数组的内容就到这里了,我们侧重的是数组这个数据结构,排序算法的具体实现以及优化操作这里不做详细讲解,可以参考最前面的链接。

希望大家能对数据形成一个如思维导图般的架构,不仅仅知道数组是一段连续内存空间所形成的结构,还要知道它的数据运算,如增删改查,以及排序,双指针,滑动窗口等各方面的应用。

这样在你遇到一个问题时,不是单纯思考用何种算法,而是结合需要用到的数据结构,它们是相辅相成的。


老规矩,有问提问,有用三连,感谢!

你可能感兴趣的:(数据结构,算法与思维,c++,数据结构,开发语言)