LeetCode—数组与字符串—编程笔记

出于找工作的需要,最近一周开始刷题了,经同学推荐发现了一个比较好的刷题网站LeetCode,最近把其中的数组与字符串栏的题目刷完了,这里进行一个总结,以将自己的劳动沉淀成自己的知识体系。

说明:由于自己水平有限,只是对C语言比较熟练,因此刷题的过程中全部采用的是C语言来实现。

说明:对于数组和字符串的基本理论知识不再做讲解,本篇文章主要记录在进行数组、字符串、二维数组的处理时常用的算法和思路。





声明

总共有以上22个题目(所有题目来源都来源于LeetCode,这里统一对题目转载进行说明,后续在出现题目时就不再进行来源转载说明了)





方法



情景1:单指针对数组进行操作

我们知道数组是可以通过下表来进行索引的,而单指针的意思就是用于索引的变量只有一个,通过单个指针的移动来不断的索引或者遍历数组中的数。
单指针的情况一般都不是比较复杂,我们在进行算法设计的时候,不需要考虑数组中多个索引之间的关系的时候,才可以使用。如果算法的设计遇到需要注意不同数组索引之间的关系、需要注意边界位置、需要同时满足一些条件等情况,那么单指针对数组进行操作往往不能满足我们的需求。

总而言之,单指针的情况适用于,一次操作只关注数组中的一个索引的情况。例如:
相关的题目有:
1.寻找数组的中心索引
2.搜索插入位置
8.最长回文子串
18.杨辉三角 II


情景2:双指针之头尾指针对数组进行操作

单指针往往不能满足较为复杂的算法。而对于数组来说,双指针是一个常用的方法,双针中,较为经典的用法之一,就是头尾指针,一个指针指向数组头,一个指针指向数组尾,每次循环两个指针相互靠近,可以是头指针向尾指针移动,也可以是尾指针向头指针移动,也可以是两个同时移动,根据自己的算法需要进行使用。直到两个指针指向的相等或位置颠倒(尾在头前),那么就实现了对数组的所有索引的操作。

头尾指针常用于对数组进行原地翻转(直接改变输入数组,改变后直接输出,不申请多余容器存放数组数据),或者原地移除数组中的数,以及需要头尾同时满足某些条件等情况。

相关的题目有:
9.翻转字符串里的单词
11.反转字符串
13.两数之和 II - 输入有序数组
14.移除元素
16.长度最小的子数组


情景3:双指针之快慢指针

双指针中的另一种用法是快慢指针,与头尾指针不同在于,头尾指针往往需要对头和尾的关系进行判断,或需要头尾替换。而快慢指针往往需要相邻或相近的索引之间具有特殊的关系,例如相邻的连续出现,或相邻的非连续个数等关注某一连续或邻近索引之间的关系。常常用于分割、移动等情景。

相关的题目有:
15.最大连续1的个数
21.删除排序数组中的重复项
22.移动零


情景4:数组先排序后处理

有些情况下,如果对于一个复杂的问题没有较好的解决方法,可以思考是否是需要对该数组先进行排序,如果说其他的算法的复杂程度和时间花费远远大于排序所需要的时间,那么先排序,后处理,有可能会将问题大幅度的简化。

在C语言中,其库函数提供了数组排序的方法,在stdlib.h中的qsort()函数。其用法是:
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void , const void))

base – 指向要排序的数组的第一个元素的指针。
nitems – 由 base 指向的数组中元素的个数。
size – 数组中每个元素的大小,以字节为单位。
compar – 用来比较两个元素的函数。

其中compar为用户自定的比较函数,例如如果需要升序排列,则:

static int cmp(const void* a, const void* b)
{
    return *(int*)a - *(int*)b; //实现升序排序
}

如果是降序,则:

int cmp(const void *a,const void *b)
{
return(*(int *)b-*(int *)a);  //实现降序排序
}

总之,如果返回一个正值,则会将a放在b后,如果返回的是负值,则会将a放在b前,如果返回的是0,则a和b的位置是不确定的。
因此还可以根据自身算法的需要来写出特殊的比较函数。

相关的题目有:
3.合并区间
12.数组拆分 I


情景5:数组的二分查找

对于一个有序的数组,或类似有序的数组,最快的查找方法就是二分查找。因此当我们需要有序或和有序相关的数组查找问题时,且要求时间复杂度尽量的小时,就可以考虑二分法进行数组查找。

相关的题目有:
2.搜索插入位置
20.寻找旋转排序数组中的最小值


情景6:二维数组

二维数组的问题一般来说不算太复杂,只需要理清楚行、列之间的关系,以及理解二维数组的本质是数组指针数组。

需要注意的是,LeetCode对于二维数组的输出有比较严格的要求,需要对二维数组进行malloc,也需要对二维数组内的行指针进行malloc,还需要对二维数组的列数数组进行malloc,还需要给列数组中各行的列数分配具体的数值。

相关的题目有:
4.旋转矩阵
5.零矩阵
6.对角线遍历
17.杨辉三角


情景7:哈希表法

对于字符串来说,其数值为ASCII码,因此可以通过一个索引个数为256的哈希表来记录字符串中的字符,从而方便我们进行快速的查找。

相关的题目有:
7.最长公共前缀


情景8:strstr()的实现

关于srtsrt的实现,是一个比较经典的问题,实现方法中比较经典的就是KMP法。
相关的题目有:
10.实现 strStr()


情景9:堆栈法

在进行字符翻转时,如果对于空间没有太多的要求,还可以采用堆栈的方法,遇到字符后压入堆栈,遇到分隔符后将单词推出,可以根据需求选择从栈顶推出还是从栈底推出。
相关的题目有:
9.翻转字符串里的单词
19.反转字符串中的单词 III


情景10:双指针之滑动指针

双指针中还有一种叫做滑动指针,其含义是一前一后两个指针像是一个可以滑动的窗口,有效的数据为两个指针之间的所有数据,每一次滑动一下指针,只需要对新增或丢弃的数进行记录,不需要从新计算所有的数。

相关的题目有:
16.长度最小的子数组





例题



1.寻找数组的中心索引

题目:

给定一个整数类型的数组 nums,请编写一个能够返回数组 “中心索引” 的方法。

我们是这样定义数组 中心索引 的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。

如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。

示例 1:

输入:
nums = [1, 7, 3, 6, 5, 6]
输出:3
解释:
索引 3 (nums[3] = 6) 的左侧数之和 (1 + 7 + 3 = 11),与右侧数之和 (5 + 6 = 11) 相等。
同时, 3 也是第一个符合要求的中心索引。

示例 2:

输入:
nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心索引。

说明:

nums 的长度范围为 [0, 10000]。
任何一个 nums[i] 将会是一个范围在 [-1000, 1000]的整数。


解法:

这里我们的思路是,定义两个容器,一个为左侧之和,一个为右侧之和,开始将所有的数加在一起作为右侧之和,然后通过单指针从左到右历遍数组,每指向一个新的数,就表明我们把该数作为中心,此时我们比较当前的左侧之和和右侧之和,就能判断当前指针是否是中心索引位置。

为了简化计算,我们每次移动指针之后,只需要左侧之和加上指针左边的数,右侧之和减去指针所指的数,而不需要每次都从新计算左右侧之和。

只需要最多历遍一次数组就能得到结果,因此时间复杂度为O(n)。
所需要的空间不随输入个数的改变而改变,为固定空间大小,因此空间复杂度为O(1)。


代码:

int pivotIndex(int* nums, int numsSize){
    if(numsSize==0) return -1;
    if(numsSize==1) return 0;
    if(numsSize==2) return -1;

    int sum_right=0;
    int sum_left=0;
    int temp;
    int i=0;
    int j=0;

    for(i=1;i<numsSize;i++)
    {
        sum_right+=nums[i];
    }
    if(sum_right==0) return 0;
    for(j=1;j<numsSize;j++)
    {
        sum_left+=nums[j-1];
        sum_right-=nums[j];
        if(sum_left==sum_right) return j;
    }

    return -1;
}


2.搜索插入位置

题目:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:

输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0

解法:

这里可以采用单指针进行逐个比较,时间复杂度为O(n),空间复杂度为O(1)。
同时我们也注意到题目中所提到的是一个已经排序的数组,对于一个已经排序的数组,最快的方法是二分法进行查找,二分法需要至少两个指针来记录当前二分段的边界。时间复杂度为O(logn),空间复杂度为O(1)。


代码:

//单指针顺序查找法
int searchInsert(int* nums, int numsSize, int target){
    int i;
    int temp=0;


    for(i=0;i<numsSize;i++)
    {
        if(target>nums[i]) temp=i+1;
        else return temp;
    }

    return temp;
}
//二分法
int searchInsert(int* nums, int numsSize, int target){
    int pLeft=0, pRight=numsSize-1, mid;
    while(pLeft <= pRight){
        mid = (pLeft + pRight) / 2;
        if(target > nums[mid])
            pLeft = mid + 1;
        else if(target < nums[mid])
            pRight = mid - 1;
        else return mid;
    }
    return target<nums[mid] ? mid : mid + 1;
}


3.合并区间

题目:

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3][2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4][4,5] 可被视为重叠区间。

解法:

这一题如果考虑用传统的数组的处理方法,会因为各个区间排布的不确定性(如大小颠倒、负数、前后颠倒等)而需要考虑大量的特殊情况,而将数组进行排序后,发现只需要看排序后的所有的左区间点和所有的右区间点之间的数字连续问题,而不需要考虑区间与区间之间的关系,因此大大简化的所需要考虑的问题。

这里我们将所有的左区间点进行排序,再将所有的右区间点进行排序,排序完成后,从最小的左区间点开始,按照右区间点判断一次,左区间点判断一次,右一次,左一次的顺序,判断区间是否连续,如果连续就继续判断下去,直到出现不连续,然后取保持连续的最后一个右区间点为子区间的右区间点。然后再开一个子区间的左区间点,重复上面的步骤。

由于首先使用了排序算法,排序的时间复杂度为O(nlogn),空间复杂度为O(logn);然后再遍历一次,遍历的时间复杂度为O(n),空间复杂度为O(n)。因此总的时间复杂度为O(nlogn),空间复杂度为O(n)。


代码:

/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */
static int cmp(const void* a, const void* b)
{
    return *(int*)a - *(int*)b;
}

int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes){

    int i;
    int j=0;
    *returnSize=0;
    if(intervalsSize==0) return intervals;

    int** returnArray = (int** )malloc(intervalsSize * sizeof(int*));
    *returnColumnSizes = (int*)malloc(intervalsSize * sizeof(int));
    int* pStart = (int*)malloc(intervalsSize * sizeof(int)); 
    int* pEnd = (int*)malloc(intervalsSize * sizeof(int));

    for(i=0;i<intervalsSize;i++)
    {
        returnArray[i]=(int*)malloc((*intervalsColSize) * sizeof(int)); 
        pStart[i]=intervals[i][0];
        pEnd[i]=intervals[i][1];
        (*returnColumnSizes)[i]=2;
    }

    qsort(pStart, intervalsSize, sizeof(int), cmp);
    qsort(pEnd, intervalsSize, sizeof(int), cmp);
    
    returnArray[0][0]=pStart[0];
    returnArray[0][1]=pEnd[0];
    for(i=0;i<intervalsSize-1;i++)
    {
        if(pEnd[i]>=pStart[i+1])
        {
            if(pEnd[i]<pEnd[i+1]) returnArray[j][1]=pEnd[i+1];
        }
        else
        {
            j++;
            returnArray[j][0]=pStart[i+1];
            returnArray[j][1]=pEnd[i+1];            
        }
    }
    *returnSize=j+1;
    return returnArray;
}


4.旋转矩阵

题目:

给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。

不占用额外内存空间能否做到?

示例 1:

给定 matrix = 
[
  [1,2,3],
  [4,5,6],
  [7,8,9]
],

原地旋转输入矩阵,使其变为:
[
  [7,4,1],
  [8,5,2],
  [9,6,3]
]

示例 2:

给定 matrix =
[
  [ 5, 1, 9,11],
  [ 2, 4, 8,10],
  [13, 3, 6, 7],
  [15,14,12,16]
], 

原地旋转输入矩阵,使其变为:
[
  [15,13, 2, 5],
  [14, 3, 4, 1],
  [12, 6, 8, 9],
  [16, 7,10,11]
]

解法:

这题如果不对空间有所要求,那么就直接可以通过另取一个容器进行暴力旋转。时间复杂度为O(MN),空间复杂度为O(MN)。
如果需要原地旋转,那么我们需要取四个指针,分别指向四个角点,然后原地变换即可。时间复杂度为O(M*N),空间复杂度为O(1)。


代码:

//暴力法,另取一个容器存放旋转后的矩阵
void rotate(int** matrix, int matrixSize, int* matrixColSize){
    int temp_matrix[matrixSize][*matrixColSize];
    int i,j;
    for(i=0;i<matrixSize;i++)
    {
        for(j=0;j<(*matrixColSize);j++)
        {
            temp_matrix[j][matrixSize-1-i]=matrix[i][j];
        }
    }
    for(i=0;i<matrixSize;i++)
    {
        for(j=0;j<(*matrixColSize);j++)
        {
            matrix[i][j]=temp_matrix[i][j];
        }        
    }
}


5.零矩阵

题目:

编写一种算法,若M × N矩阵中某个元素为0,则将其所在的行与列清零。

示例 1:

输入:
[
  [1,1,1],
  [1,0,1],
  [1,1,1]
]
输出:
[
  [1,0,1],
  [0,0,0],
  [1,0,1]
]

示例 2:

输入:
[
  [0,1,2,0],
  [3,4,5,2],
  [1,3,1,5]
]
输出:
[
  [0,0,0,0],
  [0,4,5,0],
  [0,3,1,0]
]

解法:

该题的思路是,先对整个数组遍历一次,然后得到整个数组需要置零的列和需要置零的行的信息,然后对数组再遍历一次,进行置零操作。时间复杂度为O(M*N),空间复杂度为O(M+N)。


代码:

void setZeroes(int** matrix, int matrixSize, int* matrixColSize){
    int i;
    int j;
    int k;
    if(matrixSize==0) return ;
    int* zero_j=(int*)malloc(*matrixColSize*sizeof(int));
    int* flagClear=(int*)malloc(matrixSize*sizeof(int));
    memset(zero_j,1,*matrixColSize*sizeof(int));
    memset(flagClear,0,matrixSize*sizeof(int));

    for(i=0;i<matrixSize;i++)
    {
        for(j=0;j<*matrixColSize;j++)
        {
            if(matrix[i][j]==0)
            {
                flagClear[i]=1;
                zero_j[j]=0;
            }
        }
    }

    for(i=0;i<matrixSize;i++)
    {
        if(flagClear[i]==1)
        {
            for(j=0;j<*matrixColSize;j++)
            {
                matrix[i][j]=0;
            }
        }
        else
        {
            for(j=0;j<*matrixColSize;j++)
            {
                if(zero_j[j]==0) matrix[i][j]=0;
            }
        }
    }
    free(zero_j);
    free(flagClear);
}


6.对角线遍历

题目:

给定一个含有 M x N 个元素的矩阵(M 行,N 列),请以对角线遍历的顺序返回这个矩阵中的所有元素,对角线遍历如下图所示。

示例:

输入:
[
 [ 1, 2, 3 ],
 [ 4, 5, 6 ],
 [ 7, 8, 9 ]
]

输出:  [1,2,4,7,5,3,6,8,9]

解法:

该题的意思是,从对角线对数组进行遍历,1–>2,4–>7,5,3–>6,8–>9,这里可以发现,如果有N行M列,那么总共需要按对角线遍历N+M-1次,每次对角线遍历最多有max(M,N)个数。因此我们创造一个max(M,N)的空间用于存放每次对角线遍历的数,如果是奇数次对角线遍历,就正向输出,如果是偶数次对角线遍历,就反向输出。此外从第一行开始的遍历和从最后一列开始的遍历是不一样的,因此分开来写。总的时间复杂度为O(MN),空间复杂度为O(MN)。


代码:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* findDiagonalOrder(int** matrix, int matrixSize, int* matrixColSize, int* returnSize){
    if(matrixSize==0) 
    {
        *returnSize=0;
        int* returnArray=(int*)malloc(sizeof(int)); 
        return returnArray;
    }
    *returnSize=(* matrixColSize) * matrixSize;
    int* returnArray=(int*)malloc(*returnSize*sizeof(int));
    int pArray=0;

    int num_temp=((matrixSize>=(*matrixColSize))?matrixSize:(*matrixColSize));
    int num_times=matrixSize+(*matrixColSize)-1;
    int* temp=(int*)malloc(num_temp*sizeof(int));
       
    int i,j,k,m;
    int pTemp=0;

    for(k=0;k<*matrixColSize;k++)
    {
        i=0;
        j=k-i;
        while(j>=0 && i<=matrixSize-1)
        {
            temp[pTemp]=matrix[i][j];
            i++;
            pTemp++;
            j--;
        }
        if(k%2==0)
        {
            for(m=pTemp;m>0;m--) 
            {
                returnArray[pArray]=temp[m-1];
                pArray++;
            }
        }
        else
        {
            for(m=0;m<pTemp;m++) 
            {
                returnArray[pArray]=temp[m];
                pArray++;
            }
        }
        pTemp=0;
    }

    for(k=*matrixColSize;k<num_times;k++)
    {
        j=(*matrixColSize-1);
        i=k-j;
        while(j>=0 && i<=matrixSize-1)
        {
            temp[pTemp]=matrix[i][j];
            i++;
            pTemp++;
            j--;
        }
        if(k%2==0)
        {
            for(m=pTemp;m>0;m--) 
            {
                returnArray[pArray]=temp[m-1];
                pArray++;
            }
        }
        else
        {
            for(m=0;m<pTemp;m++) 
            {
                returnArray[pArray]=temp[m];
                pArray++;
            }
        }      
        pTemp=0;
    }

    free(temp);
    
    return returnArray;

}


7.最长公共前缀

题目:

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:

输入: ["flower","flow","flight"]
输出: "fl"

示例 2:

输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

说明:

所有输入只包含小写字母 a-z 。


解法:

实际上只是用哈希表来实现一个纵向指针的偏移,这里的写法不够简洁。时间复杂度为O(M*N),空间复杂度为O(1)。


代码:

char * longestCommonPrefix(char ** strs, int strsSize){
    static char returnString[500]={0};
    int table[26]={0};
    int i=0;
    int j=0;
    char k;

    if(strsSize==0) return returnString;

    while(j<=500)
    {
        for(i=0;i<strsSize;i++)
        {
            k=strs[i][j];
            if(k == '\0')
            {
                returnString[j]='\0';
                return returnString;
            }
            else
            {
                (table[(k-'a')])++;
            }
            if((table[(strs[i][j]-'a')])!=i+1)
            {
                returnString[j]='\0';
                return returnString;
            } 
        }
        memset(table,0,26);
        returnString[j]=strs[0][j];
        j++;
    }

    return returnString;

}


8.最长回文子串

题目:

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

解法:

对于回文字符串的解法,可以采用单指针,指向的位置为回文字符串的中心,然后往两边扩展。这种方法的时间复杂度为O(n^2),空间复杂度为O(1)。


代码:

char * longestPalindrome(char * s){
    int mid_left=0;
    int mid=0;
    int mid_right=mid_left+mid+1;
    int len=strlen(s);
    int len_left=1;
    int len_right=len-len_left-mid;
    
    if(len==0) return "";
    
    int start=0;
    int end=0;
    int i=0;
    int j;
    int j_len;
    int temp_len=0;
    int max_len=0;

    while(len_left<len || max_len<j_len)
    {
        mid_left=i/2;
        mid=i%2;
        mid_right=mid_left+mid+1;
        len_left=mid_left+1;
        len_right=len-len_left-mid;
        j_len=(len_left<=len_right?len_left:len_right);
        for(j=0;j<j_len;j++)
        {
            if(s[mid_left-j]==s[mid_right+j])
            {
                temp_len=j+j+mid+2;
                if(temp_len>max_len)
                {
                    max_len=temp_len;
                    start=mid_left-j;
                    end=mid_right+j;
                }
            }
            else break;
        }
        i++;
    }

    s[end+1]='\0';

    return (s+start);

}


9.翻转字符串里的单词

题目:

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"

示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

无空格字符构成一个单词。 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

进阶:

请选用 C 语言的用户尝试使用 O(1) 额外空间复杂度的原地解法。


解法:

方法1,采用双指针的方法,先将整个数组进行原地旋转,例如a good example变为elpmaxe doog a,然后再对单个单词进行局部的原地旋转。最后只要处理好边界问题和多余空格,就能很好的实现这个算法。由于每次只需要对数组进行一次遍历,因此时间复杂度为O(n),由于是进行原地操作,因此空间复杂度为O(1)。

方法2,采用堆栈的思想,从后往前遍历,如果是非空格,就压入堆栈中,如果是空格,就将堆栈中的单词推出,如果需要单词也是翻转的,那就先入后出,如果单词不需要翻转,只需要单词顺序翻转,那么就是先入先出。这里单词不需要翻转,因此是先入先出。由于只需要对数组进行一次遍历,因此时间复杂度为O(n),空间复杂度为O(n)。


代码:

//方法一:双指针-双重翻转
void reverse(char *s, int start, int end) {
    char temp;
    while (start < end) {
        temp = s[start];
        s[start++] = s[end];
        s[end--] = temp;
    }
}

void trimSpace(char *s, int start) {
    // 将中间多余的空格移到最后,同时把字符串结束符\0向前搬一个
    do {
        s[start] = s[start+1];
        start++;
    } while (s[start]);  // 在字符串结束符停止
}

char * reverseWords(char * s){
    // 1.消除前面多余空格
    while (*s == ' ') s++;
    // 2.消除后面的空格,且长度-1
    int len = strlen(s) - 1;
    if (len < 0) return s;
    while (s[len] == ' ') {
        s[len] = '\0';
        len--;
    }
    reverse(s, 0, len);  // 整体翻转

    // 3.消除中间多余空格并反转局部
    int i, idx = 0;
    for (i = 0; s[i] != '\0'; i++) {
        if (s[i] == ' ') {  // 遇到空格表示单词结束
            reverse(s, idx, i - 1);  // 注意区间[idx,i-1]是单词
            // 准备删除第二个空格
            while (s[i+1] && s[i+1] == ' ') {
                trimSpace(s, i + 1);
                len--;  // 修改字符数组长度
            }
            idx = i + 1;  // 最后idx移到新的单词开头这里
        }
    }
    // 处理最后单词
    reverse(s, idx, len);
    return s;
}
char * reverseWords(char * s){
    // 1.消除前面多余空格
    while (*s == ' ') s++;
    // 2.消除后面的空格,且长度-1
    int len = strlen(s) - 1;
    if (len < 0) return s;
    while (s[len] == ' ') {
        s[len] = '\0';
        len--;
    }
    // 3.栈翻转单词(先进先出)
    char **stack = (char **)malloc(sizeof(char *) * (len + 2) / 2);  // 栈内指针最大不超过
    char *token = strtok(s, " "), *tmp;
    int top = 0;
    while (token != NULL) {
        // printf("token:%s\n", token);
        tmp = (char *)malloc(sizeof(char) * (len + 2));  // 索引+1=长度+1(\0)
        strcpy(tmp, token);
        stack[top++] = tmp;
        token = strtok(NULL, " ");  // 第二次传入NULL
    }
    // 4.出栈翻转
    int word_len = 0;  // 计算单词长度
    while (top) {
        tmp = stack[--top];  // 出栈元素
        strcpy(s + word_len, tmp);  // 连续在s基础上拷贝
        word_len += strlen(tmp);  // 总单词长度增加
        free(tmp);  // 拷贝后就释放
        s[word_len++] = ' ';  // 修改\0为空格再让word_len+1
    }
    free(stack);
    s[word_len-1] = '\0';  // 这里比较关键,把s末尾置0
    return s;
}


10.实现 strStr()

题目:

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1:

输入: haystack = "hello", needle = "ll"
输出: 2

示例 2:

输入: haystack = "aaaaa", needle = "bba"
输出: -1

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符


解法:

KMP法,自行搜索,主要的思想就是状态机的思想。


代码:

void getNext(char* pat, int next[]) {
	int j = 0, k = -1;
	next[0] = -1;
	int len = strlen(pat);
	
	while (j < len - 1) {
		if (k == -1 || pat[j] == pat[k]) {
			k++;
			j++;
			next[j] = k;
		}
		else {
			k = next[k];
		}
	}
}

int strStr(char* str, char* pat) {
    int str_size = strlen(str), pat_size = strlen(pat);
    if (pat_size == 0) {
        return 0;
    }
    if (str_size < pat_size) {
        return -1;
    }
	int* next = (int*)malloc(sizeof(int) * pat_size);
	getNext(pat, next);
	int j = 0, k = 0;
	while (j < str_size) {
		if (k == -1 || str[j] == pat[k]) {
			j++;
			k++;
		}
		else {
			k = next[k];
		}
		if (k == pat_size) {
            free(next);
			return (j - pat_size);
		}
	}
    free(next);
	return -1;
}


11.反转字符串

题目:

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:

输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

输入:["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

解法:

头尾指针常见的用法之一,对数组进行原地翻转。每次循环,头尾指针各加一(减一)。


代码:

void reverseString(char* s, int sSize){
    int  pLeft=0;
    int  pRight=sSize-1;
    char temp;

    while(pLeft<pRight)
    {
        temp=s[pLeft];
        s[pLeft]=s[pRight];
        s[pRight]=temp;
        pLeft++;
        pRight--;
    }
}


12.数组拆分 I

题目:

给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。

示例 1:

输入: [1,4,3,2]
输出: 4
解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4).

解法:

很简单就能发现,为了使得min(ai, bi) 总和最大,只需将相邻大小的数组合成子数组即可。因此先排序,然后取相邻的数为子数组。排序的时间复杂度为O(nlogn),空间复杂度为O(logn),遍历的时间复杂度为O(n),空间复杂度为O(1)。因此总的时间复杂度为O(nlogn),空间复杂度为O(logn)。


代码:

int cmp ( const void *a , const void *b )
{
return *(int *)a - *(int *)b;
}

int arrayPairSum(int* nums, int numsSize){
    int returnInt=0;
    int n=numsSize/2;
    int i;

    qsort(nums, numsSize, sizeof(int), cmp);
    for(i=0;i<n;i++)
    {
        returnInt+=nums[i+i];
    }
    return returnInt;
}


13.两数之和 II - 输入有序数组

题目:

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

返回的下标值(index1 和 index2)不是从零开始的。

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 27 之和等于目标数 9 。因此 index1 = 1, index2 = 2

解法:

这里由于是需要两个数之和,因此很容易就能联想到用两个指针索引来表征需要求和的两个数。由由于该数组已经排好序了,那就意味着,指针增加,那么其和也增加,指针减小,那么其和也减小。因此我们用头尾两个指针,如果其和大于目标数,那么意味着我们需要减小我们的右指针,如果其和小于目标数,那么意味着我们需要增加我们的左指针,直到其和等于目标数,那么此时指针指向的两个数就是结果,或者当头尾两个指针相遇时都没有符合的结果,那么就表明没有解。由于只需要对数组进行一次遍历就能求解,因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* twoSum(int* numbers, int numbersSize, int target, int* returnSize){
    int* returnInds=(int* )malloc(2 * sizeof(int));
    if(numbersSize==0)
    {
        *returnSize=0;
        return returnInds;
    }

    int pLeft=0;
    int pRight=numbersSize-1;

    while(pLeft!=pRight)
    {
        if(numbers[pLeft]+numbers[pRight]==target)
        {
            *returnSize=2;
            returnInds[0]=pLeft+1;
            returnInds[1]=pRight+1;
            return returnInds;
        }
        else if(numbers[pLeft]+numbers[pRight]<target)
        {
            pLeft++;
        }
          else if(numbers[pLeft]+numbers[pRight]>target)
        {
            pRight--;
       }
    }
    *returnSize=0;
    return returnInds;

}


14.移除元素

题目:

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 nums = [0,1,2,2,3,0,4,2], val = 2,

函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

注意这五个元素可为任意顺序。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}


解法:

这里是原地修改数组,因此自然就想到采用双指针来实现。又通过阅读题目,其目的是将需要的数移到数组靠前的位置,将不需要的数字移动到数组靠后的位置。因此自然想到采用头尾双指针,头指针指向的位置为存放需要输出的数,尾指针指向的位置存放因该舍弃的数。头尾不断靠近,遍历一次后就能将靠前的位置排满需要输出的数,将靠后的位置排满需要舍弃的数。因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

int removeElement(int* nums, int numsSize, int val){
    if(numsSize==0) return 0;
    int pLeft=0;
    int pRight=numsSize-1;
    int temp;

    while(pLeft!=pRight)
    {
        if(nums[pLeft]==val)
        {
            temp=nums[pRight];
            nums[pRight]=nums[pLeft];
            nums[pLeft]=temp;
            pRight--;
        }
        else
        {
            pLeft++;
        }
    }
    if(nums[pLeft]!=val) pLeft++;

    return pLeft;
}


15.最大连续1的个数

题目:

给定一个二进制数组, 计算其中最大连续1的个数。

示例 1:

输入: [1,1,0,1,1,1]
输出: 3
解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.

注意:

输入的数组只包含 0 和1。
输入数组的长度是正整数,且不超过 10,000。


解法:

需要对连续出现的1计数,因此容易想到采用快慢双指针的方法。慢指针指向连续出现的第一个1,快指针不断增加直到指向的数不是1,从而实现了对连续个1的索引。剩下的只需要计数即可。由于只需要遍历一次就能得到解,因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

int findMaxConsecutiveOnes(int* nums, int numsSize){
    int pSlow=0;
    int pFast=0;
    int max_len=0;
    int temp_len=0;

    while(pFast<numsSize)
    {
        if(nums[pSlow]==0)
        {
            pSlow++;
            pFast=pSlow;
        }
        else 
        {
            while(pFast<numsSize && nums[pFast]==1) pFast++;
            temp_len=pFast-pSlow;
            max_len=max_len>temp_len?max_len:temp_len;
            pSlow=pFast;
        }
    }
    return max_len;
}


16.长度最小的子数组

题目:

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

进阶:

如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。


解法:

采用头尾指针的方法,实际上更像是采用滑动窗口的方法,首先取头尾指针,取头尾指针之间的数为子数组。开始的时候所有的数为子数组,判断是否满足>=s的条件,如果满足,尝试将尾指针-1,即尝试舍弃最后一个数,如果舍弃后仍满足>=s的条件,那么就继续尝试舍弃子数组的最后一个数。如果不满足>=s的条件,那么就将头指针和尾指针同时+1,滑动指针窗口,看滑动后的子数组是否满足条件。如此循环,直到子数组的个数为1,或者窗口滑动到底也无法满足条件。由于采用滑动窗口的方法,因此只需要最多遍历2次就能得到解,因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

int minSubArrayLen(int s, int* nums, int numsSize){
    int min_len=0;
    int len=numsSize;
    int pLeft=0;
    int pRight=numsSize-1;
    int sum=0;

    if(numsSize==0) return 0;
    for(int i=0;i<numsSize;i++) sum+=nums[i];
    if(sum<s) return 0;

    while(len>0)
    {
        if(sum>=s)
        {
            min_len=len;
            sum=sum-nums[pRight];
            len--;
            pRight--;
        }
        else
        {
            sum=sum-nums[pLeft];
            pLeft++;
            pRight++;
            if(pRight>=numsSize) return min_len;
            sum=sum+nums[pRight];
        }
    }


    return min_len;

}


17.杨辉三角

题目:

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

示例:

输入: 5
输出:
[
     [1],
    [1,1],
   [1,2,1],
  [1,3,3,1],
 [1,4,6,4,1]
]

解法:

没什么好讲的,看懂杨辉三角的来历就能写出来,暴力解法,挨个计算就好。时间复杂度O(N^2),空间复杂度O(N^2)


代码:

/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */
int** generate(int numRows, int* returnSize, int** returnColumnSizes){
    
    *returnSize=numRows;
    int** returnArray=(int**)malloc(numRows*sizeof(int*));
    *returnColumnSizes = (int*)malloc(numRows * sizeof(int));

    int i,j;

    for(i=0;i<numRows;i++)
    {
        returnArray[i]=(int*)malloc((i+1)*sizeof(int));
        (*returnColumnSizes)[i]=i+1;
    }

    if(numRows==0) return returnArray;
     
    returnArray[0][0]=1;
    if(numRows==1) return returnArray;

    returnArray[1][0]=1;
    returnArray[1][1]=1;   
    if(numRows==2) return returnArray;

    for(i=2;i<numRows;i++)
    {
        returnArray[i][0]=1;
        returnArray[i][i]=1;
        for(j=1;j<i;j++)
        {
            returnArray[i][j]=returnArray[i-1][j-1]+returnArray[i-1][j];
        }
    }
    return returnArray;

}


18.杨辉三角 II

题目:

给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。

示例:

输入: 3
输出: [1,3,3,1]

进阶:

你可以优化你的算法到 O(k) 空间复杂度吗?


解法:

和杨辉三角的解法类似,关键在于如果对空间有要求,那么需要在原数组的基础上进行原地计算,为了避免原地计算的结果覆盖还没有计算的数,这里我们的指针从数组的中间开始倒着计算,此外由于杨辉三角是对称的,因此我们只需要每次算一半就好。时间复杂度为O(N^2),空间复杂度为O(N)。


代码:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* getRow(int rowIndex, int* returnSize){
    *returnSize=rowIndex+1;
    int* returnArray=(int*)malloc((rowIndex+1)*sizeof(int));

    int pMid=0;
    int flag=0;
    int i,j;
    
    if(rowIndex<0) return returnArray;

    returnArray[0]=1;
    if(rowIndex==0) return returnArray;

    returnArray[1]=1;
    if(rowIndex==1) return returnArray;   

    for(i=2;i<(*returnSize);i++)
    {
        pMid=i/2;
        flag=i%2;
        for(j=pMid;j>0;j--)
        {
            returnArray[j]+=returnArray[j-1];
        }
        if(flag==0)
        {
            for(j=1;pMid+j<=i;j++)
            {
                returnArray[pMid+j]=returnArray[pMid-j];
            }
        }
        else
        {
            for(j=1;pMid+j<=i;j++)
            {
                returnArray[pMid+j]=returnArray[pMid-j+1];
            }            
        }
    }
    return returnArray;

}


19.反转字符串中的单词 III

题目:

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例 1:

输入: "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc" 

注意:在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。


解法:

这题对于空间没有要求,而且还需要保留单词顺序,只是翻转单个单词,因此可以采用堆栈来进行单词的存储和推出。单指针增加,碰到字符则压入堆栈,碰到分隔符(空格),就推出,且是先入后出,即可实现。时间复杂度为O(n),空间复杂度为O(n)。
如果需要原地旋转,则可以采用局部头尾指针来实现。


代码:

char * reverseWords(char * s){
    int len_s=strlen(s);
    if(len_s==0) return "";
    char* tempWord=(char*)malloc((len_s+1)*sizeof(char));
    char* tempStr=(char*)malloc((len_s+1)*sizeof(char));
    int pTempWord=0;
    int pTempStr=0;
    int pStr=0;

    for(;pStr<len_s;pStr++)
    {
        if(s[pStr]!=' ')
        {
            tempWord[pTempWord]=s[pStr];
            pTempWord++;
        }
        if(s[pStr]==' ' || pStr==len_s-1)
        {
            for(;pTempWord>0;pTempWord--)
            {
                tempStr[pTempStr]=tempWord[pTempWord-1];
                pTempStr++;
            }
            tempStr[pTempStr]=' ';
            pTempStr++;
        }
    }
    free(tempWord);
    tempStr[pTempStr-1]=0;

    return tempStr;

}


20.寻找旋转排序数组中的最小值

题目:

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 1:

输入: [3,4,5,1,2]
输出: 1

示例 2:

输入: [4,5,6,7,0,1,2]
输出: 0

解法:

该题的巧妙之处在于,虽然经过翻转后,整个数组已经不再是完美的升序的数组,但是同样可以采用二分法来进行处理,因为存在一个关系就是,如果说区域内是包含翻转数字的,那么区域的头指针一定是大于尾指针的。通过这种方法我们就能不断的缩小区域范围,找到包含翻转数字的最小区间。二分查找的时间复杂度为O(logn),空间复杂度为O(1)。


代码:

//二分法

int findMin(int* nums, int numsSize){

    int left=0;
    int right=numsSize-1;

    while(right>left) 
    { 
        int mid=left+(right-left)/2;
        if(nums[mid]>nums[right])
            left=mid+1;
        else
            right=mid;
    }
    return nums[left];


}


21.删除排序数组中的重复项

题目:

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

给定数组 nums = [1,1,2], 

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 

你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 for (int
i = 0; i < len; i++) {
print(nums[i]); }


解法:

这里同样是需要找到连续出现的数,因此可以采用快慢双指针的方法来实现。慢指针指向一个数,快指针向后索引,如果索引到的数相同,则快指针继续向下索引,如果不同,则表明数不再连续,将新的数移到慢指针的下一位,然后慢指针加一,然后快指针继续向下索引,直到索引到数组尾。由于只需要快指针遍历一次,因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

int removeDuplicates(int* nums, int numsSize){
    int pSlow=0;
    int pFast=0;

    if(numsSize==0) return 0;
    if(numsSize==1) return 1;

    for(pFast=1;pFast<numsSize;pFast++)
    {
        if(nums[pFast] != nums[pSlow])
        {
            nums[pSlow+1]=nums[pFast];
            pSlow++;
        }
    }
    return pSlow+1;

}


22.移动零

题目:

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。


解法:

这里由于要求尽量在原数组中进行操作,因此容易想到,采用双指针然后进行双指针之间的数值替换。同时题目还要求保留除了零之外的数字的顺序,因此不能采用头尾指针,而是采用快慢双指针。慢指针指向数字为零的地点,快指针找到后面的数字不为零的地点,然后两个指针进行替换,从而将0不断的往后放,而将后面的数字按顺序不断地在前面填满。由于只需要快指针历遍一次,因此时间复杂度为O(n),空间复杂度为O(1)。


代码:

void moveZeroes(int* nums, int numsSize){
    int pSlow=0;
    int pFast=0;

    if(numsSize==0) return nums;
    for(pFast=0;pFast<numsSize;pFast++)
    {
        if(nums[pSlow]==0 && nums[pFast]!=0)
        {
            nums[pSlow]=nums[pFast];
            pSlow++;
            nums[pFast]=0;
        }
        else if(nums[pSlow]!=0) pSlow++;
    }
    return nums;

}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值