算法基础(一)(共有25道例题,大多数为简单题)

一、枚举(Enumerate)算法

定义:就是一个个举例出来,然后看看符不符合条件。
举例:一个数组中的数互不相同,求其中和为0的数对的个数。

for (int i = 0; i < n; ++i)
  for (int j = 0; j < i; ++j)
    if (a[i] + a[j] == 0) ++ans;
例题1:计数质数

题目 难度:中等

  1. 方法1:枚举(会超时)
    考虑到如果 y 是 x 的因数,那么 x y \dfrac{x}{y} yx也必然是 x 的因数,因此我们只要校验 y 或者 x y \dfrac{x}{y} yx即可。而如果我们每次选择校验两者中的较小数,则不难发现较小数一定落在 [ 2 , x ] [2,\sqrt{x}] [2,x ]的区间中,因此我们只需要枚举 [ 2 , x ] [2,\sqrt{x}] [2,x ]中的所有数即可。
class Solution {
public:
    bool isPrime(int x) //判断x是否是质数
    {
        for (int i = 2; i * i <= x; ++i) 
        {
            if (x % i == 0) 
            {
                return false;//不是质数
            }
        }
        return true;//是质数
    }

    int countPrimes(int n) {
        int ans = 0;
        for (int i = 2; i < n; ++i) {
            ans += isPrime(i);
        }
        return ans;
    }
};
  1. 方法2:埃氏筛
    如果 x 是质数,那么大于 x 的倍数 2x,3x,…一定不是质数,因此我们可以从这里入手。
class Solution 
{
public:
    int countPrimes(int n) 
    {
        vector<int> isPrime(n, 1);//一开始全标记为1
        int ans = 0;
        for (int i=2; i<n; ++i) 
        {
            if (isPrime[i]) 
            {
                ans += 1;//2,3必是质数               
                for (int j=2*i; j<n; j+=i) 
                {
                    isPrime[j]=0;
                    //i是质数,i的倍数(j)肯定不是质数,赋值为0
                }                
            }
        }
        return ans;
    }
};
例题2:等差素数列

题目 难度:简单

#include 
using namespace std;

//判断n是否是素数
int check(int n)
{
  for(int i=2;i<n;i++)
  {
    if(n%i==0) return 0;//不是素数
  }
  return 1;//是质数
}

int main()
{
  int len=0;
  int gc;//公差
  int num;//序列的第一个数
  int ans=0;

  for(num=2;num<=1000;num++)
  {
    if(check(num))//检查第一个数字是不是素数
    {
      for(gc=1;gc<=1000;gc++)//从1开始枚举公差
      {
        for(int j=1;j<1000;j++)
        {
          if(check(num+j*gc)) ans++;//代表是素数
          else 
          {
            ans=0;
            break;//推出当前for循环
          }
          if(ans==9) //从0开始计数,ans=0时就已经有一个
          {
            cout<<gc;
            return 0;
          }
        }
      }
    }
  }
  return 0;
}
例题3:统计平方和三元组的数目

题目 难度:简单

class Solution {
public:
    int countTriples(int n) 
    {
        int ans=0;
        for(int a=1;a<=n;a++)
        {
            for(int b=a+1;b<+n;b++)
            {
                for(int c=b+1;c<=n;c++)
                {
                    if(a*a+b*b==c*c) ans+=1;                   
                }
            }
        }
        return ans*2;
    }
};
例题4:算术三元组的数目

题目 难度:简单

  1. 方法1:暴力
class Solution {
public:
    int arithmeticTriplets(vector<int>& nums, int diff) 
    {
        int size=nums.size();
        int num=0;//算术三元组的数目

        for(int i=0;i<size;i++)
        {
            for(int j=i+1;j>i&&j<size;j++)
            {
                if(nums[j]-nums[i]==diff)
                {
                    for(int k=j+1;k>j&&k<size;k++)
                    {
                        if(nums[k]-nums[j]==diff) 
                        {
                            num=num+1;
                            break;//退出当前for(k)循环
                        }
                        else continue;
                    }
                } 
                else continue;           
            }
        }
        return num;    
    }
};
  1. 方法2:哈希表,用哈希表记录每个元素,然后遍历 nums,看 nums[j]−diff 和 nums[j]+diff 是否都在哈希表中。
class Solution {
public:
    int arithmeticTriplets(vector<int>& nums, int diff) {
        int n = nums.size();
        unordered_set<int> st;
        for (int x : nums) st.insert(x);//将nums数组里的值插入到哈希表st中    
            
        int ans = 0;
        for (int i = 0; i < n; i++) 
        {
            if (st.count(nums[i] +diff) > 0 && st.count(nums[i] + 2*diff) > 0) 
            {
                ans++;
            }
        }
        return ans;
    }
};
例题5:公因子的数目(会)

题目 难度:简单

class Solution {
public:
    int commonFactors(int a, int b) 
    {
        int min_data=min(a,b);//找a,b之间的最小值
        int num;
        for(int i=1;i<=min_data;i++)//公因数一定小于a和b的最小值
        {
            if(a%i==0&&b%i==0)//公因子的定义
            {
                ++num;
            }
        }
        return num;        
    }
};
例题6:买钢笔和铅笔的方案数(会)

题目 难度:中等

class Solution {
public:
    long long waysToBuyPensPencils(int total, int cost1, int cost2) 
    {
        int total1=total/cost1;//最多买几支钢笔
        int total2=total/cost2;//最多买几支铅笔
        long long num=0;
        for(int i=0;i<=total1;i++)
        {                
            if(total-i*cost1>=0)//买完钢笔后还能买几只铅笔
            {
                int new_total=total-i*cost1;
                num+=new_total/cost2; 
                num=num+1; 
            }        
        }
        return num;
    }
};
例题7:个位数字为 K 的整数之和(不会)

题目 难度:中等

class Solution {
public:
    int minimumNumbers(int num, int k) 
    {
        if(num==0) return 0;//当 num=0时,唯一的方法是选择一个空集合,答案为0
        for(int i=1;i<=10;i++)//num>0时,我们可以发现最多不会选择超过10个数。
        //这是因为如果这些数的个位数字为 k,并且我们选择了至少 11个数,由于11*k(10*k+k)的个位数字也为k,那么我们可以把任意的11个数合并成1个,使得选择的数仍然满足要求,并且集合更小。
        {
            if(i*k<=num&&(num-i*k)%10==0) return i;
            //i*k<=num:由于每个数最小为 k,那么这 i 个数的和至少为 i⋅k。如果i⋅k>num,那么无法满足要求。
            //这 i 个数的和的个位数字已经确定,即为 i*k mod 10。
            //我们需要保证其与 num 的个位数字相同,这样 num−i⋅k 就是 10 的倍数,我们把多出的部分加在任意一个数字上,都不会改变它的个位数字。
        } 
        return -1;      
    }
};

二、模拟算法

定义:模拟就是用计算机来模拟题目中要求的操作。

例题8:爬动的蠕虫

题目 难度:简单

#include 
using namespace std;
#include 

int main()
{
    int n,u,d;
    cin>>n>>u>>d;
    int x=0;//爬的高度
    int num=0;//爬的分钟数
    while (true) 
    {  // 用死循环来枚举
        x += u;
        num++;//爬一分钟
        if (x>= n) break;  // 满足条件则退出死循环
        num++;//休息一分钟
        x -= d;
    }
    cout<<num;
    return 0;
}
例题9:基于排列构建数组

题目 难度:简单

class Solution {
public:
    vector<int> buildArray(vector<int>& nums) 
    {
        int size=nums.size();
        vector<int> ans(size,0);

        for(int i=0;i<size;i++)
        {
            ans[i]=nums[nums[i]];
        }
        return ans;
    }
};
例题10:方程整数解

题目 难度:简单

#include 
using namespace std;
int main()
{
  // 请在此输入您的代码
  for(int i=1;i<1000;i++)
  {
    for(int j=i;j<1000;j++)
    {
      for(int k=j;k<1000;k++)
      {
        if(i*i+j*j+k*k==1000&&i!=6&&j!=8&&k!=30)
        {
          cout<<min(min(i,j),k);
          return 0;
        }
      }
    }
  }
  return 0;
}
例题11:等差数列

题目 难度:简单

#include 
using namespace std;
#include 
#include 
int main()
{
    // 请在此输入您的代码
    int n;
    cin >> n;
    vector<int> arr(n);
    int diff;
    int ans = n;
    for (int i = 0; i < n; i++)
    {
        cin >> arr[i];
    }
    sort(arr.begin(), arr.end());
    //cout << "从小到大排序后:";
    //for (int i = 0; i < n; i++)
    //{
        //cout << arr[i] <<" ";
    //}
    //cout << endl;
    vector<int> diff_vector(n-1);
    int min_value;
    for (int j = 0; j < n -1; j++)
    {
        diff_vector[j] = arr[j + 1] - arr[j];
    }
    min_value = *min_element(diff_vector.begin(), diff_vector.end());
    //cout << min_value << endl;;

    for (int k = 0; k < n - 1; k++)
    {
        if (arr[k + 1] - arr[k] != min_value)
        {
            diff = arr[k + 1] - arr[k];
            ans += (diff / min_value)-1;
        }
    }
    if (arr[1] == arr[0]) cout << n;
    else cout << ans;
    return 0;
}
例题12:方格填数(不会)

题目 难度:简单

#include 
#include
using namespace std;
int main()
{
  // 请在此输入您的代码
  int ans=0;//填数方案
  int num[]={0,1,2,3,4,5,6,7,8,9};
  
  do{
      if(abs(num[0]-num[1])!=1&&abs(num[0]-num[3])!=1
      &&abs(num[0]-num[4])!=1&&abs(num[0]-num[5])!=1
      &&abs(num[1]-num[4])!=1&&abs(num[1]-num[2])!=1
      &&abs(num[1]-num[5])!=1&&abs(num[1]-num[6])!=1
      &&abs(num[2]-num[5])!=1&&abs(num[2]-num[6])!=1
      &&abs(num[3]-num[4])!=1&&abs(num[3]-num[7])!=1&&abs(num[3]-num[8])!=1
      &&abs(num[4]-num[5])!=1&&abs(num[4]-num[7])!=1
      &&abs(num[4]-num[8])!=1&&abs(num[4]-num[9])!=1
      &&abs(num[5]-num[8])!=1&&abs(num[5]-num[9])!=1&&abs(num[5]-num[6])!=1
      &&abs(num[6]-num[9])!=1&&abs(num[7]-num[8])!=1&&abs(num[8]-num[9])!=1){ans++;}
    }while(next_permutation(num,num+10));
    cout<<ans<<endl;

  return 0;
}

next_permutation 全排列函数
需要引用的头文件:

#include 

函数原型:

bool std::next_permutation<int *>(int *_First, int *_Last)

(1)基本格式:

int a[];
do{
//循环体
}while(next_permutation(a,a+n));//表达式
//全排列生成好了next_permutation函数返回0,会跳出while循环。

(2)举例:

#include 
#include 
#include 
#using namespace std;
int main()
{
    string s = "aba";
    sort(s.begin(), s.end());//排序aab
    do {
        cout << s << '\n';//先执行一次这行,再去执行while的表达式
    } while(next_permutation(s.begin(), s.end()));
    //do…while 是先执行一次循环体,然后再判别表达式。
    //当表达式为“真”时,返回重新执行循环体,如此反复,直到表达式为“假”为止,此时循环结束。
}
//输出:aab 
//aba
//baa
#include
#include
using namespace std;
int main() 
{
	int a[4] = { 0 }, n;
	cin >> n;
	for (int i = 0; i <n; i++)
	{
		cin >> a[i];
	}
	do{
		for (int i = 0; i <n; ++i)
		{
			cout << a[i] << " ";
		}
		cout << endl;
	} while (next_permutation(a, a + n));
	return 0;
}
//输入:4
//2 5 1 3
//输出:
//2 5 1 3
//2 5 3 1
//3 1 2 5
//3 1 5 2
//3 2 1 5
//3 2 5 1
//3 5 1 2
//3 5 2 1
//5 1 2 3
//5 1 3 2
//5 2 1 3
//5 2 3 1
//5 3 1 2
//5 3 2 1

三、递归(Recursion)算法

递归代码最重要的两个特征:结束条件自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。
递归的缺点:在程序执行中,递归是利用堆栈来实现的。每当进入一个函数调用,栈就会增加一层栈帧,每次函数返回,栈就会减少一层栈帧。而栈不是无限大的,当递归层数过多时,就会造成栈溢出的后果。
如何优化递归:深度优先搜索(DFS)/记忆化搜索(动态规划的一种)

例题13:剑指 Offer 64. 求1+2+…+n

题目 难度:中等
使用递归会出现的问题:终止条件需要使用 if ,因此本方法不可取。
思考:除了 if还有什么方法?答:逻辑运算符

a&&b
如果a是false,那么就不会继续执行b
a||b
如果a是true,就不会继续执行b

class Solution {
public:
    int num=0;
    int sum=0;//总和
    int sumNums(int n) 
    {
        //递归
        num=n;
        sum+=num;
        --num;
        num>=1&&sumNums(num);//也可换成num==0||sumNums(num);
        return sum;
    }
};
例题14:2 的幂

题目 难度:简单

class Solution {
public:
    bool isPowerOfTwo(int n) 
    {
        if(n==1) return true;//终止条件
        if(n<=0) return false;
        bool res=false;
        if(n%2==0)
        {
            n=n/2;
            res=isPowerOfTwo(n);
        }
        return res;
    }
};
例题15:斐波那契数

题目 难度:简单

class Solution {
public:
    int fib(int n) 
    {
    	//截至条件
        if(n==0) return 0;
        if(n==1) return 1;
        //递推关系
        return fib(n-1)+fib(n-2);
    }
};

四、十大排序算法

术语:
(1)稳定排序:如果 a 原本在 b 的前面,且 a=b,排序之后 a 仍然在 b 的前面,则为稳定排序。
(2)非稳定排序:如果 a 原本在 b 的前面,且 a=b,排序之后 a 可能不在 b 的前面,则为非稳定排序。
(3)原地排序:原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
(4)非原地排序:需要利用额外的数组来辅助排序。

(一)选择排序

  1. 定义:首先,找到数组中最的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。其次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法我们称之为选择排序。
  2. 性质:(1)时间复杂度: O ( n 2 ) O(n^2) O(n2) (2)空间复杂度: O ( 1 ) O(1) O(1) (3)非稳定排序 (4)原地排序
  3. 优点:不占用额外的内存空间。缺点:时间复杂度高
//从小到大排序
template<typename T> 
void selection_sort(vector<T>& arr) 
{
	for (int i = 0; i < arr.size()-1; i++) 
    {
    	int min = i;
        for (int j = i + 1; j < arr.size(); j++)
        {
        	if (arr[j] < arr[min])
            {
            	min = j;//记录最小值
            }
        }
       swap(arr[i], arr[min]);//交换i与min对应的值
    }
}
例题16: 按身高排序

题目 难度:简单

class Solution {
public:
    vector<string> sortPeople(vector<string>& names, vector<int>& heights) 
    {
        for (int i = 0; i < heights.size()-1; i++) 
        {
    	    int max = i;
            for (int j = i + 1; j < heights.size(); j++)
            {
        	    if (heights[j] > heights[max])
                {
            	    max = j;//记录最大值
                }
            }
            swap(heights[i],heights[max]);//交换i与max对应的值
            swap(names[i], names[max]);
        }
        return names;  
    }
};

(二)插入排序(是指插到右边)

  1. 定义:插入排序(Insertion sort)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为已排序和未排序两部分,每次从未排序的元素中选择一个插入到已排序的元素中的正确位置。从数组第2个(从i=1开始)元素开始抽取元素key,把它与左边第一个(从j=i-1开始)元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比key大的元素(arr[j] <= key),然后插到这个元素的右边(arr[j+1] = key)。继续选取第3,4,…n个元素。
    如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
  2. 性质:(1)时间复杂度: O ( n 2 ) O(n^2) O(n2) (2)空间复杂度: O ( 1 ) O(1) O(1) (3)稳定排序 (4)原地排序
//从小到大排序
void insertion_sort(int a[], int n)
{
    for(int i= 1; i<n; i++)
    {  
        int key = a[i];//从第二个元素开始抽取数据key
        int j= i-1;//i-1是因为要从key左边第一个元素开始比较
        while(j>=0&&a[j]>key)//key与下标位j,j-1....的元素一个个比较过去。采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
        {  
            a[j+1] = a[j];
            j--;
        }
        a[j+1] = key;//插入到正确位置,这里j+1是因为上面的j--
    }
}
例题17: 有序数组的平方

题目 难度:简单

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) 
    {
    	//耗时太长
        //先平方整个数组
        for(int i=0;i<nums.size();i++)
        {
            nums[i]=nums[i]*nums[i];
        }
        //插入排序     
        for(int i=1;i<nums.size();i++)
        {
            int key=nums[i];
            int j=i-1;
            while(j>=0&&key<nums[j])//当key>=nums[]时,把key插到num[i]的右边
            {
                nums[j+1]=nums[j];
                j--;
            }
            nums[j+1]=key;//这里j+1是因为上面j--
        }
        return nums;
    }
};
例题18:对链表进行插入排序

题目难度:中等

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* insertionSortList(ListNode* head) 
    {
       if (head == nullptr) 
       //首先判断给定的链表是否为空,若为空,则不需要进行排序,直接返回。
       {
            return head;
        }
        ListNode* dummyHead = new ListNode(0);
        //创建哑节点 dummyHead,令 dummyHead.next = head。
        //引入哑节点是为了便于在 head 节点之前插入节点。
        dummyHead->next = head;
		//维护lastSorted为链表的已排序部分的最后一个节点,初始时 lastSorted = head。
        ListNode* lastSorted = head;//已经排好序的最后一个节点
        
        ListNode* curr = head->next;//维护 curr 为待插入的元素,初始时 curr = head.next
        //举例head = [-1,5,3,4,0]
        while(curr!=nullptr)
        {
            if(curr->val>=lastSorted->val)//5>-1
            //说明curr应该位于 lastSorted 之后,将 lastSorted 后移一位,curr 变成新的 lastSorted。
            {
                lastSorted=lastSorted->next;
            }
            else//3<5
            //否则,从链表的头节点开始往后遍历链表中的节点,寻找插入 curr的位置。令 prev为插入 curr的位置的前一个节点,进行如下操作,完成对curr的插入
            {
                ListNode *prev=dummyHead;
                while (prev->next->val <= curr->val) 
                //从头开始遍历,直到遇到一个prev->next->val > curr->val,那么curr就要插到prev与prev->next之间:prev->curr->prev.next
                {
                    prev = prev->next;
                }
                lastSorted->next = curr->next;//这里不懂?
                curr->next=prev->next;
                prev->next=curr;
            }
            //令curr=lastSorted.next,curr就成为下一个待插入的元素
            curr=lastSorted->next;//每排完一个数字,就把curr往后移动
        }
        return dummyHead->next;
    }
};

(三)希尔排序

  1. 定义:希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
    希尔排序是基于插入排序的以下两点性质而提出改进方法的:插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
    希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
  2. 性质:(1)时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn) (2)空间复杂度: O ( 1 ) O(1) O(1) (3)非稳定排序 (4)原地排序
void shell_sort(vector<int>& arr,int n)
{
	int key;
	int j;
	//初始增量inc为n/2
	for(int inc=n/2;inc>0;inc/=2)//假设inc=12/2,6/2=3,3/2=1;
	{
		//每一趟采用插入排序
		for(int i=inc;i<n;i++)//搞清楚这里为什么是i++,因为是从inc开始一直到n-1,让inc和0,inc+1和1,inc+2和2……n-1与n-1-inc两两相比较
		{
			key=arr[i];
			for(j=i;j>=inc&&arr[j-inc]>key;j-=inc)
			{
				arr[j]=arr[j-inc];
			}
			arr[j]=key;
		}
	}
}
例题19: 排序数组

题目 难度:中等

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        int n=nums.size();
        int key;
        int j;
	//初始增量inc为n/2
	    for(int inc=n/2;inc>0;inc/=2)//假设inc=12/2,6/2=3,3/2=1;
	    {
		//每一趟采用插入排序
		    for(int i=inc;i<n;i++)//搞清楚这里为什么是i++,因为是从inc开始一直到n-1,让inc和0,inc+1和1,inc+2和2……n-1与n-1-inc两两相比较
		    {
			    key=nums[i];
			    for(j=i;j>=inc&&nums[j-inc]>key;j-=inc)
			    {
				    nums[j]=nums[j-inc];
			    }
			    nums[j]=key;
		    }
	    }
        return nums;
    }
};

(四)冒泡排序

  1. 定义:冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
    从小到大排序的步骤:
    (1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
    (2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
    (3)针对所有的元素重复以上的步骤,除了数列末尾已经排序好的元素。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
  2. 性质:1、时间复杂度: O ( n 2 ) O(n^2) O(n2) 2、空间复杂度: O ( 1 ) O(1) O(1) 3、稳定排序 4、原地排序
    算法基础(一)(共有25道例题,大多数为简单题)_第1张图片
#include 
using namespace std;
template<typename T> //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符
//从小到大排序
void bubble_sort(T arr[], int len) //len是arr的长度
{
    int i, j;
    for (i = 0; i < len - 1; i++)//轮数
    {
        for (j = 0; j < len - 1 - i; j++)//每一轮要交换几次
        {
        	if (arr[j] > arr[j + 1])
        	{
        		swap(arr[j], arr[j + 1]);
        	}
         }
	}
}
int main() {
        int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
        int len = (int) sizeof(arr) / sizeof(*arr);
        bubble_sort(arr, len);
        for (int i = 0; i < len; i++)
                cout << arr[i] << ' ';
        cout << endl;
        float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
        len = (float) sizeof(arrf) / sizeof(*arrf);
        bubble_sort(arrf, len);
        for (int i = 0; i < len; i++)
                cout << arrf[i] << ' '<<endl;
        return 0;
}

(五)计数排序

  1. 定义:计数排序(Counting sort)是一种线性时间的排序算法。它的工作原理是使用一个额外的计数数组 C,其中数组C中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数,然后根据数组 C 来将 A 中的元素排到正确的位置。计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。
    算法的步骤如下:
    (1)找出待排序的数组中最大和最小的元素,那么计数数组C的长度为最大值减去最小值+1。
    (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项。
    (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)。
    (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
  2. 性质:(1)时间复杂度: O ( n + k ) O(n+k) O(n+k) (2)空间复杂度: O ( k ) O(k) O(k) (3)稳定排序 (4)非原地排序
//a是待排序数组,b是排序后的数组,cnt是额外数组
//从小到大排序
void counting_sort() 
{
	if(a.size()<2) return;
	//寻找最大元素
	//int max=a[0];
	//for(int i=0;i
	//{
		//if(a[i]>max) max=a[i];
	//}
	int max = *max_element(heights.begin(), heights.end());
	// void *memset(void *str, int c, size_t n) :复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
	//分配一个长度为(max-min+1)的计数数组来存储计数
   // memset(cnt, 0, sizeof(int)*(max-min+1));//cnt变成全0数组,清零
    vector<int> count(max+1,0);//下标为0,1,2,……,max
	//计数
    for (auto x:a) ++cnt[x];//统计数组中每个值为x的元素出现的次数,存入数组cnt的第x项
    
    for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];//统计出现次数,w是cnt的长度
    
    for (int i=0;i<len;i++

) 
    {
    	b[cnt[a[i]]-1] = a[i];
    	--cnt[a[i]];
    }//反向填充目标数组,n是数组a的长度
}
例题20:高度检查器

题目 难度:简单
注意到本题中学生的高度小于等于100,因此可以使用计数排序。

class Solution {
public:
    int heightChecker(vector<int>& heights) 
    {
        int len=heights.size();
        int num=0;
        
        //寻找最大元素
	    // int max=a[0];
	    // for(int i=0;i
	    // {
		//     if(a[i]>max) max=a[i];
	    // }
        int max = *max_element(a.begin(), a.end());
        //计数数组初识化为0
        vector<int> count(max+1,0);//下标为0,1,2,……,max
        //计数
        for(int i=0;i<len;i++)
        {
            count[heights[i]]++;
        }
        //统计计数的累计值
        for(int i=1;i<max+1;i++)
        {
            count[i]+=count[i-1];
        }
        //创建输出数组expected
        vector<int> expected(len);
        for(int i=0;i<len;i++)
        {
            expected[count[heights[i]]-1]=heights[i];//count[heights[i]]-1是元素正确的位置
            count[heights[i]]--;
        }
        //找不同的下标数量
        for(int i=0;i<len;i++)
        {
            if(heights[i]!=expected[i]) num++;
        }
        return num;
    }
};

(六)快速排序

  1. 定义:快速排序(Quicksort),又称分区交换排序(partition-exchange sort),简称快排,是一种被广泛运用的排序算法。
  2. 使用了分治法(Divide-and-Conquer Method)。
    该方法的基本思想是:
    (1)先从数列中取出一个数作为基准数pivot。一般如何挑基准数呢?①:取第一个元素。(通常选取第一个元素)②:取最后一个元素③:取中间位置的元素④:取第一个和最后一个之间位置的随机数 k (low<=k<=hight)
    (2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
    (3)再对左右区间重复第二步,直到各区间只有一个数。
  3. 性质:(1)平均情况下的时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn);最佳时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn);最差时间复杂度: O ( n 2 ) O(n^2) O(n2)(2)空间复杂度:略(3)不稳定排序 (4)原地排序

方法1:双指针法

 int partition(int r[], int low, int high)  //划分函数
{
	int i = low, j = high, pivot = r[low]; //基准元素
	while (i < j)
	{
		while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
		{
			j--;
		}
		if (i < j)//退出while循环,这里其实已经找到了一个小于等于pivot的数
		{
			swap(r[i], r[j]); 
			i++; //r[i]和r[j]交换后 i 向右移动一位
		}
		while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
		{
			i++;
		}
		if (i < j)
		{
			swap(r[i], r[j]);  //r[i]和r[j]交换后 i 向左移动一位
			j--;
		}
	}
	return i;  //返回最终划分完成后基准元素所在的位置
}

 void QuickSort(int A[], int low, int high) //快排母函数
 {
   if (low < high) 
   {
     int pivot = Paritition(A, low, high);
     QuickSort(A, low, pivot - 1);
     QuickSort(A, pivot + 1, high);
   }
 }

方法2:挖坑填数法

int partition(int r[], int l, int r)
{
    if (l< r)
    {      
        int i = l, j = r, x = r[l];//相当于r[l]被挖掉存在x里面了
        while (i < j)
        {
            while(i < j && r[j]>= x) // 从后向前找第一个小于x的数,找到就r[i]=r[j]
            {
            	j--;
            } 
            if(i < j)
            {
                r[i] = r[j];
                i++;
            }
            while(i < j && r[i]< x) // 从前向后找第一个大于等于x的数
                i++; 
            if(i < j)
            {
                r[j] = r[i];
                j--;
            }
        }
        return i;
    }
}
 void QuickSort(int A[], int low, int high) //快排母函数
 {
   if (low < high) 
   {
     int pivot = Paritition(A, low, high);
     QuickSort(A, low, pivot - 1);
     QuickSort(A, pivot + 1, high);
   }
 }
例题21:785快速排序(模板

题目

#include
using namespace std;
const int N = 1e6 + 10;

int n;
int arr[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);

        //上面两行do while 执行完后,相当于i指向>=x,j指向<=x
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

int main()
{
    cin >> n;

    for (int i = 1; i <= n; i++)
    {
        cin >> arr[i];
    }

    quick_sort(arr, 1, n );

    for (int i = 1; i <= n; i++)
    {
        cout << arr[i] << " ";
    }

    return 0;
}

(七)归并排序

  1. 定义:归并排序也是基于分治思想,将数组分段排序后合并。
  2. 性质:(1)时间复杂度在最优、最坏与平均情况下均为 O ( n l o g n ) O (nlogn) O(nlogn),(2)空间复杂度为 O ( n ) O (n) O(n)。(3)稳定排序
例题22:787. 归并排序(模板

题目

#include
using namespace std;

int n;
const int N=1e6+10;
int arr[N];
int tmp[N];

void merge_sort(int arr[],int left,int right)
{
    if(left==right) return;//截至条件
    
    int mid=left+right >> 1;
    
    //递归,一直递归到mid-left=1
    merge_sort(arr,left,mid);
    merge_sort(arr,mid+1,right);
    
    //合并
    int i=left,j=mid+1, k=0;
    while(i<=mid&&j<=right)
    {
        if(arr[i]<=arr[j]) tmp[k++]=arr[i++];
        else tmp[k++]=arr[j++];
    }
    while(i<=mid) tmp[k++]=arr[i++];
    while(j<=right) tmp[k++]=arr[j++];
    
    //tmp赋值给原来的数组arr
    for (i = left, j = 0; i <= right; i ++, j ++ ) arr[i] = tmp[j];
}

int main()
{
    cin >> n;

    for (int i = 0; i < n; i++)
    {
        cin >> arr[i];
    }

    merge_sort(arr, 0, n-1 );

    for (int i =0; i <n; i++)
    {
        cout << arr[i] << " ";
    }

    return 0;
}

(八)堆排序

  1. 定义:堆排序(Heapsort)可以说是一种利用二叉堆这种数据结构所设的选择排序算法。堆排序的适用数据结构为数组。分为两种方法:
    (1)大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
    (2)小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
  2. 步骤:
    (1)创建一个堆 H [ 0 … … n − 1 ] H[0……n-1] H[0……n1]
    (2)把堆首(最大值)和堆尾互换;
    (3)把堆的尺寸缩小 1,并调用 shift_down,目的是把新的数组顶端数据调整到相应位置;
    (4)重复步骤 2,直到堆的尺寸为1。
  3. 性质:(1)平均时间复杂度为 Ο(nlogn)。(3)不稳定排序 (4)原地排序
  4. 有数组中下标为 i 的节点,对应的父结点、左子结点和右子结点如下:

下标为i的节点的父节点下标:iParent(i) = (i - 1) / 2;[整数除法]
下标为i的节点的左孩子节点下标:iLeftChild(i) = 2 * i + 1;
下标为i的节点的右孩子节点下标:iRightChild(i) = 2 * i + 2;

//每调用一次shift_down就是比较一个节点(父节点)和他的子节点们(1个或者2个)的大小,从最后一个节点的父节点开始比较
void shift_down(int arr[], int start, int end) 
{
  // 计算父结点和子结点的下标
  int parent = start;//假设start为父节点
  int child = parent * 2 + 1;//child是左孩子,child+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;//左孙节点
    }
  }
}

void heap_sort(int arr[], int len) 
{
  // 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)
  //最后一个节点是len-1,他的父节点是(len-1-1)/2
  for (int i = (len - 1 - 1) / 2; i >= 0; i--) 
  {
  	shift_down(arr, i, len - 1);//把最大的元素放到顶部
  }
  // 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕
  for (int i = len - 1; i > 0; i--) 
  {
    swap(arr[0], arr[i]);
    sift_down(arr, 0, i - 1);
  }
}

int main() 
{
    int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
    int len = arr.size();
    //int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << " ";
    cout << endl;
    return 0;
}

(九)桶排序

  1. 定义:桶排序(Bucket sort)是排序算法的一种,适用于待排序数据值域较大但分布比较均匀的情况。将数据分到有限数量的桶里,然后对每个桶分别排序,最后把全部桶的数据合并。
  2. 步骤:
    (1)设置一个定量的数组当作空桶;
    (2)遍历序列,并将元素一个个放到对应的桶中;
    (3)对每个不是空的桶进行排序;
    (4)从不是空的桶里把元素再放回原来的序列中。
    补充:实际开发中用链表
const int N = 100010;

int n, w, a[N];
vector<int> bucket[N];

//对每个桶里面的数使用插入排序
void insertion_sort(vector<int>& A) 
{
  for (int i = 1; i < A.size(); ++i) 
  {
    int key = A[i];
    int j = i - 1;
    while (j >= 0 && A[j] > key) 
    {
      A[j + 1] = A[j];
      --j;
    }
    A[j + 1] = key;
  }
}

void bucket_sort() 
{
  int bucket_size = w / n + 1;//桶的大小,n是桶的个数,w是要排序的数有多少个
  for (int i = 0; i < n; ++i) 
  {
    bucket[i].clear();
  }
  for (int i = 1; i <= n; ++i) 
  {
    bucket[a[i] / bucket_size].push_back(a[i]);
  }
  int p = 0;
  for (int i = 0; i < n; ++i) 
  {
    insertion_sort(bucket[i]);
    for (int j = 0; j < bucket[i].size(); ++j) 
    {
      a[++p] = bucket[i][j];//a是最后排好序的数组
    }
  }
}

(十)基数排序

  1. 定义:基数排序(Radix sort)是一种非比较型的排序算法,最早用于解决卡片排序的问题。
int maxbit(int data[], int n) //辅助函数,求数据的最大位数,比如说最大数是3356,那么最大位数是4
{
    //int maxData = data[0];              ///< 最大数
    /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
    //for (int i = 1; i < n; ++i)
    //{
        //if (maxData < data[i]) maxData = data[i];
    //}
    int maxData = *max_element(data.begin(), data.end());
    int d = 1;
    int p = 10;
    while (maxData >= p)
    {
        maxData /= 10;
        ++d;
    }
    return d;//d是最大位数

//另外一种求最大位数的方法,不用求最大值maxData,直接遍历每个数,就是可能会overflow
/*  int d = 1; //保存最大的位数
    int p = 10;
    for(int i = 0; i < n; ++i)
    {
        while(data[i] >= p)
        {
            p *= 10;
            ++d;
        }
    }
    return d;*/
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int *tmp = new int[n];
    int *count = new int[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) //进行d次排序
    {
        for(j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for(j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }
    delete []tmp;
    delete []count;
}

补充:C++中 new int[] 和 new int() 的区别
(1)int *p = new int[3];//申请一个动态整型数组,数组的长度为3。 value: p[i];addr: &p[i]
(2)int *p = new int(3); // p指向一个值为3的int数。value: *q;addr: q

五、搜索算法

数据结构 空间
DFS stack O ( h ) O(h) Oh不具有最短路性质
BFS queue O ( 2 h ) O(2^h) O2h 具有最短路性质

h是高度,所以DFS用的空间比BFS小。

(一)深度优先搜索DFS

所谓深度优先,就是说每次都尝试向更深的节点走。DFS包含概念回溯和剪枝。每个DFS都对应一个搜索树。

例题23:排列数字

题目 难度:中等
算法基础(一)(共有25道例题,大多数为简单题)_第2张图片

#include
using namespace std;

const int N=10;
int n;//总共有多少层
int path[N];//记录方案
bool st[N];//标记数组 哪些数被用过了,true代表这个数被用过了   一开始默认全为0也就是false

void dfs(int u)//u代表在第几层了,u=0在第一层,u=1在第二层.....
{
    if(u==n)//叶节点
    {
        for(int i=0;i<n;i++) cout<<path[i]<<" ";
        cout<<endl;
        return;//直接退出这个dfs函数,到上一个dfs函数
    }
    
    for(int i=1;i<=n;i++)
    {
        if(!st[i])
        {
            path[u]=i;
            st[i]=true;//代表点i已经被遍历过
            dfs(u+1);            
            //到底部时,回溯+恢复现场
            //path[u]=0;
            st[i]=false;
        }
    }
}
int main()
{
    cin>>n;    
    dfs(0);//一开始在第0层    
    return 0;
}

算法基础(一)(共有25道例题,大多数为简单题)_第3张图片

例题23:全排列

题目 难度:中等
给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。你可以按任意顺序返回答案。注意如果要求按照字典顺序输出,就必须使用标记数组。

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;

    void dfs(vector<bool>& st,vector<int>& nums)
    {
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return;
        }

        for(int i=0;i<nums.size();i++)
        {
            if(st[i]==false)
            {
                path.push_back(nums[i]);
                st[i]=true;
                dfs(st,nums);
                //回溯,恢复现场
                path.pop_back();
                st[i]=false;
            }
            else if(st[i]==true) continue;//就行下一个for循环
        }
    }
    vector<vector<int>> permute(vector<int>& nums) 
    {       
        vector<bool> st(nums.size(), false);//标记数组st,一开始默认都是false
        result.clear();
        path.clear();
        //使用DFS方法  
        dfs(st,nums);

        return result;
    }
};
例题24:n-皇后问题

这题的标记数组就不止一个了,有三个:(1)col[ ]代表列,(2)dg[ ]代表副对角线,(3)udg[ ]代表主对角线。
斜角判断法:
我们可以对4x4正方形进行一个枚举:
(0,0) (0,1) (0,2) (0,3)
(1,0) (1,1) (1,2) (1,3)
(2,0) (2,1) (2,2) (2,3)
(3,0) (3,1) (3,2) (3,3)
坐标可以理解为row和i (row是第几行,i是第几列)。

(1)每条从右上到左下的对角线(副对角线),i+row的值也列出来。
(0) (1) (2) (3)
(1) (2) (3) (4)
(2) (3) (4) (5)
(3) (4) (5) (6)
(4) (5) (6) (7)
横纵坐标相加的值都是相等的,这条对角线上的点都可以通过row+i(也就是x+y),也就是横纵坐标的和来表示这个dg[row + i]是否为true。

(2)从左上角到右下角的对角线(主对角线),n+i-row的值也列出来。
(4) (5) (6) (7)
(3) (4) (5) (6)
(2) (3) (4) (5)
(1) (2) (3) (4)
横纵坐标相减都等于一个值i-row(也就是y-x),那为何这里要加上n呢,因为i-row的值有的为负,但是数组下标不可为负,所以加上n。那么这条对角线上的点都可以通过n+i-row(也就是n+y-x),来表示这个udg[n+i-row]是否为true。

#include 
using namespace std;

const int N = 20;
int n;
char g[N][N];
bool col[N], dg[N], udg[N];//一开始默认为false,即没被用过

void dfs(int row)//表示第row行皇后放在哪
{
    if (row == n)
    {
        for (int i = 0; i < n; i ++ ) puts(g[i]);
        puts("");
        return;
    }

    for (int i = 0; i < n; i ++ )第row行的皇后可以放在第i列吗
        if (!col[i] && !dg[row + i] && !udg[ - row + i])
        {
            g[row][i] = 'Q';
            col[i] = dg[row + i] = udg[ - row + i] = true;//表示用过
            dfs(row + 1);
            //回溯
            col[i] = dg[row + i] = udg[ - row + i] = false;
            g[row][i] = '.';
        }
}

int main()
{
    cin >> n;
    for (int i = 0; i <n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';

    dfs(0);
    return 0;
}

倘若不想用标记矩阵,可以使用一个check函数:check(int x,int y),检查一下在第x行上,能不能把皇后放在第y列上面,但也是使用了斜角判断法。

#include
using namespace std;
int n;
const int N=10;
int a[N];//a[i]表示第i行的皇后放在第a[i]列上
char g[N][N];

bool check(int x,int y)//第x行上,能不能把皇后放在第y列上面
{
    for(int i=0;i<x;i++)//在前面的行上面,有没有同一列或同一斜线上有皇后
    {
        if(a[i]==y) return false;
        if(i+a[i]==x+y) return false;
        if(i-a[i]==x-y) return false;
    }
    return true;
}

void dfs(int row)//表示第row行皇后放在哪
{
    if(row==n)//产生了一组解
    {
        for(int i=0;i<n;i++) cout<<g[i]<<endl;
        puts("");
        return;
    }
    
    for(int i=0;i<n;i++)
    {
        if(check(row,i))//第row行的皇后可以放在第i列吗
        {
            a[row]=i;
            g[row][i]='Q';
            dfs(row+1);
            //回溯
            a[row]=0;
            g[row][i]='.';
        }
    }
}

int main()
{
    cin>>n;//有n个皇后
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<n;j++)
        {
            g[i][j]='.';
        }
    }        
    dfs(0);    
    return 0;
}

(二)广度优先搜索BFS

所谓广度优先,就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。

例题25:走迷宫

题目 难度:中等

#include
#include
using namespace std;

const int N=110; //n,m的数据范围均为100

int maze[N][N],dis[N][N],n,m; //maze[N][N]用来存储迷宫//dis[x][y]用来存储(x,y)这一点到坐标原点的距离

queue <pair<int,int>> q;//q队列用来存储宽度优先搜素到的路径也就是走迷宫经过哪些点

int bfs()
{
    memset(dis,-1,sizeof dis); //将dis数组所有元素初始化为-1,表示这个点没有走过
    dis[0][0]=0; //位于原点(0,0)到原点的距离为0
    q.push({0,0}); //将原点入队

    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1}; //定义方向向量一共四个方向

    while(!q.empty()) //当队列非空时执行循环
    {
        auto t=q.front();//取出第一个点
        q.pop(); //插入一个位置的同时会弹出一个位置保证循环可以正常终止

        for(int i=0;i<4;i++) //遍历四个方向
        {
            int x=t.first+dx[i],y=t.second+dy[i]; //四个方向对应x,y坐标

            if(x>=0 && x<n && y<m && y>=0 && maze[x][y]==0 && dis[x][y]==-1 )//x,y都要在迷宫坐标范围内,并且(x,y)不是墙壁且没被走过
            {
                dis[x][y]=dis[t.first][t.second]+1; //走到下一个点的同时距离加1
                q.push({x,y}); //将该点入队尾
            }
        }
    }
    return dis[n-1][m-1];//dis[n-1][m-1]是指坐标为(n-1,m-1)的点距离坐标(0,0)的距离
}

int main()
{
    cin>>n>>m; //输入迷宫的尺寸大小
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            cin>>maze[i][j]; //输入迷宫,比如输入maze[1][1]=0,maze[1][2]=1
        }
    }

    cout<<bfs()<<endl; //输出宽度优先搜索结果
    return 0;
}

你可能感兴趣的:(C++刷题,算法,leetcode,c++)