算法学习第二天 -简单排序与对数器、二分与异或操作

本文介绍了三种基础排序算法——选择排序、冒泡排序和插入排序,分析了它们的时间复杂度和稳定性,并提供了实现代码。此外,讨论了如何生成随机数组,以及二分查找的应用,包括寻找特定元素和寻找大于等于特定值的最左位置。最后,利用异或操作解决寻找数组中奇数次出现的元素的问题。
摘要由CSDN通过智能技术生成

算法学习第二天了,接触了一些简单的算法

简单排序

排序算法有很多,像计数排序、桶排序、归并排序、插入排序等等。先挑三个最简单的来学,分别是选择排序、冒泡排序、插入排序,均以升序排列来说。

选择排序

思路:
从数组头部位置开始,挑选出包含该位置的余下数组中的最小值将其与该位置元素交换位置,而后从下一位置继续重复上述步骤直至最后。(本质就是从前往后缩减数组长度并不断查找最小值)
算法代码:

void mySort(vector<int> a[])
{
    int temp;
    for(int i=0; i<a.size(); i++)
    {
        int minpos=i;//最小值下标;
        for(int j=i+1; j<a.size(); j++)
        {
            minpos=a[j]<a[minpos]?j:minpos;
        }
        Swap(&a[minpos],&a[i]);
    }
}

思考:
代码中每次都要遍历余下数组,仅仅只取一个最小值不免浪费,可以同时取余下数组最大值与余下数组最后位置交换,实现两头缩减,可以大大缩减时间复杂度。
总结:
选择排序时间复杂度为O(n^2),是一个不稳定排序。(所谓不稳定排序,是指在排过序后,值相同的元素位置可能发生改变。)

冒泡排序

思路:
遍历数组,每次只比较当前元素与下一元素,大于则交换位置(每次遍历总会将大值排后,那么每次必然会将前方数组中的最大值排到后方,也就是说后方数组变得有序)
算法代码:

void mySort(vector<int> a)
{
    for(int i=0; i<a.size(); i++)
    {
        for(int j=0; j<a.size()-i-1; j++)
        {
            if(a[j+1]<a[j])
            Swap(&a[j+1],&a[j]);
        }
    }
}

思考:
当给定的数组有序递增时,依照上述代码仍要进行n,n-1……次数的比较。第一次遍历n次即为遍历整个数组,可以考虑设置个flag来查看第一次遍历是否进行了交换操作。没交换证明数组有序递增,便结束所有遍历,如此仅执行了n次,降低了在最好情况下算法的时间复杂度。
总结:
改进后的冒泡排序时间复杂度为仍为O(n^2),但是其最好时间复杂度为O(n)。与选择排序不同,它是一个稳定排序。

插入排序

思路:
如同打牌时理牌一样,从数组头位置开始,每次向后一个元素并向头位置进行倒叙遍历,比较该元素与其前方数组元素的大小,小于其前方数组元素则交换位置。
算法代码:

void mySort(vector<int> a)
{
    for(int i=1; i<a.size(); i++)
    {
        for(int j=i-1; j>=0&&a[j]>a[j+1]; j--)
        {
            Swap(&a[j+1],&a[j]);
        }
    }
}

思考:
插入排序的实质是不断扩展数组长度,并将数组中最后一元素插入至合适位置,而相邻元素比较不免过于麻烦,是否可以通过查找到前方数组中大于且最接近于该元素值的元素位置进行交换?插入排序好像冒泡排序,像是局部的部分冒泡排序。
总结:插入排序与冒泡排序相似,时间复杂度为O(n^2),最好时间复杂度也为O(n),也是稳定排序。

对数器

当写算法时需要大量随机数组,手写当然不现实,有个对数器就很棒。
对数器需要产生随机数,要用到如下代码及头文件

#include<stdlib.h>
#include <time.h>

srand(time(nullptr));//设置随机数种子

int randnum = rand() % b ;// [0,b) 范围内到随机数  

int randnum= a + rand() % ( b -a ) ;// [a,b) 范围内随机数 

在C/C++中由电脑产生随机数不难,但是难的是如何在函数中产生随机长度的随机数组并返回数组后在函数外使用时获得随机数组长度。当使用常规定义的int数组实现时会发现,返回的值为数组的一个指向数组开始位置的指针,无法获取其长度。此时可以采用C++的容器vector来实现。vector特有的操作a.size()可以轻松获得容器的长度。以插排为例,上代码:

#include <iostream>
#include <algorithm>
#include<stdlib.h>
#include <time.h>
#include <vector>
#include <cstdlib>
using namespace std;

vector<int> generateRandomArray(int maxSize,int maxValue);
void print(vector<int> a);
void mySort(vector<int> &a);

int main()
{

    int testtime=500000;//定义测试次数
    int maxSize=100;//定义随机数组长度最大值
    int maxValue=100;//定义随机数组元素最大值
    bool succed=true;
    for(int i=0; i<testtime; i++)
    {
        srand(time(nullptr));
        vector<int> a(generateRandomArray(maxSize,maxValue));//将generateRandomArry中返回的数组赋值给a
        vector<int> a1(a);
        sort(a1.begin(),a1.end()); //系统自带排序算法
        mySort(a);
        if(a!=a1)
        {
            succed=false;
            print(a);
            print(a1);
            break;
        }
   }
    if(succed==false)
        cout<<"wrong"<<endl;
    else
       cout<<"oh,Yeah"<<endl;
    return 0;
}

void mySort(vector<int> &a)//插排算法
{
    for(int i=1;i<a.size();i++)
        for(int j=i-1;j>=0&&a[j]>a[j+1];j--)
            swap(a[j+1],a[j]);
}
//产生随机长度的随机数组
vector<int> generateRandomArray(int maxSize,int maxValue)
{
    vector<int> arr(rand()%(maxSize+1));//数组随长度
    for(int i=0; i<arr.size(); i++)
        arr[i]=rand()%(maxValue+1);//数组值随机
    return arr;
}

void print(vector<int> a)
{
    for(int i=0; i<a.size(); i++)
        cout<<a[i]<<" ";
    cout<<endl;
}

二分查找

基本用途:
在有序数组中进行特定元素的查找
思路:
(1)确定并判断数组中间位置元素是否为所查找元素,是则返回中间元素位置,否则进行下一步
(2)如果查找元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区域查找,然后重复步骤(1)的操作。
算法代码:

int binarySearch(vector<int>a,int n)
{
    int L=0;
    int R=a.size()-1;
    while(L<=R)
    {
        
        int mid=L+((R-L)>>1);
        if(a[mid]==n)
            return mid;
        else if(a[mid]<n)
            L=mid+1;
        else if(a[mid]>n)
            R=mid-1;

    }
    return -1;
}

小进阶:
给定了一串有序数组要求查找数组中大于等于某个数的最左位置
思路:
传统二分查找是不断改变边界取中间值直到找到要找的元素位置,而题中要求查找大于都等于某个数的最左位置,那么当数组中有没有该数或者存在多个大于等于该数的相同元素时,传统二分查找是无法进行的。
对二分法进行一些改进,当中间值找到大于等于该元素的数值时,保存该位置,继续对剩下的区域进行二分查找是否有大于等于该数的元素,有则保存位置,直至左右边界相等。
算法代码:

int binaryNumLeft(vector<int>a,int value)
{
    int L=0;
    int R=a.size()-1;
    int mid;
    int index=-1;
    while(L<=R)
    {
        mid=L+((R-L)>>1);
        if(a[mid]>=value)
            {
                R=mid-1;
                index=mid;
            }
        else
            L=mid+1;
    }
    return index;
}

局部最小问题:
给定一组无序数组a,数组元素不重复,当出现如下情况时
1.a[0]<a[1],称a[0]为局部最小
2.a[i-1]>a[i]&&a[i]<a[i+1],称a[i]为局部最小
3.a[n]<a[n-1],称a[n]为局部最小
请给出数组中任意一个局部最小情况。
思考:
1.数组中局部最小存在的可能性:
当数组首位均不满足时,即a[0]>a[1],a[n]>a[n-1],可以看到数组左边开始是递减的趋势,假设不存在中间局部最小的情况,那么数组从左往右必然是一直递减的,而数组在其最右方两个元素是呈现递增趋势的,假设不成立,数组中间必有一局部最小来改变这个递减趋势。
2.二分查找实现的可能性:
(1)判定数组头和尾是否满足局部最小原则,满足,则返回满足元素
(2)(1)中条件不满足则截取数组中间位置与其前后进行判断,看是否满足局部最小原则,满足则返回该元素
(3)(2)中条件也不满足,则该元素必然会与数组的前或后构成与思路1中不满足的情况,那么就可以重新划分左右边界,重复(2)的步骤,直到找到满足数组局部最小原则的元素。
实现代码:

int binaryMinNum(vector<int>a,int value)
{
    if(a.empty()||a.size()<2)
        return 0;
    if(a[0]<a[1])
        return a[0];
    if(a[a.size()-1]<a[a.size()-2])
        return a[a.size()-1]
               int L=0;

    int R=a.size()-1;
    int mid;
    
    while(L<R)
    {
        mid=L+((R-L)>>1);
        if(a[mid]<a[mid-1]&&a[mid]<a[mid+1])
            return a[mid];
        else if(a[mid]>a[mid+1])
        {
            L=mid+1;
        }
        else
            R=mid-1;
    }
    return a[L];
}

数的位运算–异或

运算方法:
当两个数进行异或运算时,记为同一位置上相同为0,不同为1(稍显复杂),也可记为两数二进制无进位相加。
异或是满足交换律结合律的。
0^N=N;
N^N=0;
那么利用上述方法,可以进行一些神奇的操作:

交换数值

平时我们进行数值交换时会采用如下方法:

void mySwap(int *a,int *b)
{
	int temp;
	temp=*a;
	*a=*b;
	*b=temp;
}

该方法不会额外空间复杂度,一些时候要求可能不会过。此时可以采用另一种方法:

	a+=b;
	b-=a;
	a-=b;

该方法不会增加额外空间复杂度,但是不能进行函数封装,而且加减容易产生越界,适合小数值使用。
还可以采用另一种方法:

void mySwap(int &a,int &b)
{
    a=a^b;
    b=a^b;
    a=a^b;
}

该方法同样不会增加复杂度,但是当&a,&b指向同一个地址时,会清零,慎用。

基础版奇数次的数

给定一个数组,其中只有一个数出现了奇数次,其他数均出现了偶数次,找出并打印该数字。
思路:
利用0N=N;和NN=0且异或满足交换律结合律的性质,将数组中所有的数异或后得到的即为所取数字。
算法代码:

int mySearch(vector<int>a)
{
    int eor=0;
    for(int i=0;i<a.size();i++)
        eor^=a[i];
    return eor;
}

引入一个额外的位运算小算法:

最右方的1

给定一个int类型数(原码),要求去除原码二进制最右方的1。
将原码全部取反并末尾加一(补码)后与发现只有原码最右方1位置上的数与补码相同,其余均相反。那么将原码与上反码,便得到了最右边的1。

a//原码
temp//所求最右方的1
temp=a&((~a)+1);

进阶版奇数次的数

在基础版的基础上,数组中多出现了一个为奇数次的数,要求找到并打印这两个奇数
思路:
设这两个数为a,b。
1.将数组中所有的数异或以后得到的值为a^b
2.取ab最右方的1。a^b最右方位置上的1表示了a,b的二进制数在该位置上不同,同时,这个1也将数组分为了在该位置上为1和在该位置上为0两大块数,a和b各在其中一块
3.将其中一块数进行异或操作得到的便是其中之一的数
算法代码:

void printOddTimesNum(vector<int> a)
{
    int eor=0;
    for(int i=0;i<a.size();i++)
        eor^=a[i];
    int right1=eor&((~eor)+1);
    int eor1=0;
    for(int i=0;i<a.size();i++)
        if(a[i]&right1!=0)
            eor1^=a[i];
    cout<<eor1<<eor^eor1;
}

以上就是今天所学内容

睡过头了!!!拍桌!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值