线性表 -- 数组和链表

数组(顺序表)

基本操作

/* 初始化数组为 0 */
void initZero(int *nums, int numsSize){
    int i = 0;
    while(i < numsSize)
        nums[i++] = 0;
}

/* 交换数组下标 i,j 对应的值 */
void swap(int *nums, int i, int j){
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

/* 另一个等价的方法,不过调用时格式为 swap(&nums[i], &nums[j]) */
void swap(int *ni, int *nj){
    int t = *ni;
    *ni = *nj;
    *nj = t;
}


/* 返回关键字首次出现的位置,未找到返回 -1 */
int find(int *nums, int val){
    int i, length;
	length = sizeof(nums) / sizeof(int);
	
    for (i = 0; i < length && nums[i] != val; i++)
        ;
    if (i == length)
        return -1;
    else 
        return i;
}

/* 倒转数组 */
void reverse(int *nums, int begin, int end){
    int i = begin;

    while(i < end){
        swap(&nums[i++], &nums[end--]);
    }
}

/* 返回数组中的最大值 */
int maxNumber(int *nums, int numsSize){
    int i, max;
    for (i = 0, max = INT_MIN; i < numsSize; i++){
        if (max < nums[i])
            max = nums[i];
    }
    return max;
}

/* 返回数组中最大值的下标 */
int maxIndex(int *nums, int numsSize){
    int i, max, maxi;
    for (i = 0, max = INT_MIN; i < numsSize; i++){
        if (max < nums[i]){
            max = nums[i];
            maxi = i;
        }
    }
    return maxi;
}

/* 对数组中所有数加 val */
void addVal(int *nums, int numsSize, int val){
    int i = 0;
    while(i < numsSize)
        nums[i++] += val;
}

算法题

1. 从位置 i 插入一个数(假设数组长度足够)

void insert(int *nums, int i, int numsSize, int data){
    /* 下标从后到i,各向右移动一位,若从i开始向右移动,则需要保存下一个数的值,否则会因覆盖丢失 */
    int cur = numsSize; /* numsSize为当前存储的数据长度 */
    while(cur-- > i)
		nums[cur + 1] = nums[cur];
 	nums[cur + 1] = data; /* 此时cur = i-1,故需要+1在位置 i 处插入数据(当然,直接 nums[i] 也可) */
}

2. 循环右移 k 位( LeetCode 189题)

解法1: 通过三次反转达到移动的效果

算法思路:有两个思路原理一致的方式

(方式一)先旋转前numsSize-k%numsSize,再旋转后k%numsSize个,最后整体旋转,可以达到循环右移k位的效果,即循环左移numsSize-k位

(方式二)先整体旋转,再旋转前k%numsSize位和后numsSize-k%numsSize位,可循环左移k位

注:取余可以跳过多余的移动

/* 方式一 */
void rotate(int* nums, int numsSize, int k){
    int r = k % numsSize;
    
    reverse(nums, 0, numsSize-r-1); /* reverse() 见基础操作 */
    reverse(nums, numsSize-r, numsSize-1);
    reverse(nums, 0, numsSize-1);

}

/* 方式二 */
void rotate(int* nums, int numsSize, int k){
    int r = k % numsSize;
    
    reverse(nums, 0, numsSize-1); /* reverse() 见基础操作 */
    reverse(nums, 0, r-1);
    reverse(nums, r, numsSize-1);
}

3. 删除有序表中的重复元素

算法思路:若数组内元素小于 2,直接返回 numsSize,不需对 nums 进行修改。

因为有序,直接用 tmp 记录上个重复的数,初始化 tmp 为数组中的第一个元素。

从第二个元素开始对比,用 n 记录重复次数,重复时 +1,不重复时将数组内元素向前移动 n 位。

/* 删除有序数组内的重复元素,并返回数组剩余的元素个数 */
int removeDuplicates(int* nums, int numsSize){
    int i, tmp, n; /* tmp 记录上个重复元素,n 记录重复次数 */

    if(numsSize < 2){
        return numsSize;
    }
    tmp = nums[0];
    for (i = 1, n = 0; i < numsSize; i++){
        if (tmp == nums[i])
            n++;
        else{
            tmp = nums[i];
            nums[i - n] = nums[i]; 
        }
    }
    return numsSize - n;
}

4. 返回数组中前 k 个高频( LeetCode 189题)

解法1: 暴力解法

算法思路:创建两个数组:数组 numsTimes 记录对应 nums 的频数,数组 frequent 记录前 k 个高频对应的值。(虽然代码不复杂,但其实算是暴力解法)

(最初想法:建立一个数组记录频数,一个链表记录出现过的数,最后根据链表查找,后来发现创建多个链表结构会报错,可能是平台原因,故改成下方代码 )

int maxNumber(int *nums, int numsSize); /* 返回数组中的最大值 */
int maxIndex(int *nums, int numsSize); /* 返回数组中最大值的下标 */
void initZero(int *nums, int numsSize);

/* 仅能处理数组元素全大于 0 的情况 */
int* topKFrequent_pos(int* nums, int numsSize, int k){
    int i, cur, maxi;

    int max = maxNumber(nums, numsSize);
    int numsTimes[max+1]; 
    initZero(numsTimes, max+1);

    for (i = 0; i < numsSize; i++){
        cur = nums[i];
        ++numsTimes[cur]; /* 记录频数 */
    }

    int *frequent = (int *)malloc(sizeof(int) * k); /* 因为需返回数组,故需要malloc分配空间 */
    i = 0;
    while(i < k){
        maxi = maxIndex(numsTimes, max+1);
        frequent[i++] = maxi;
        numsTimes[maxi] = 0; /* 将当前最高频数置为0 */
    }
    
    return frequent;
}

/* 对于所有情况 */
void addVal(int *nums, int numsSize, int val);
int* topKFrequent(int* nums, int numsSize, int k){
	/* 思路同上,只是先对数组做个预处理,让其中的值必然大于0 */
    int i, cur, maxi;

    int max = abs(maxNumber(nums, numsSize)); /* 取其绝对值是为了处理存在负数的情况 */
    addVal(nums, numsSize, max);
    
    int numsTimes[2 * max + 1]; /* 2*max+1 是因为此时 nums 数组中最大的是 max+|max|,+1 是为了直接以其值作为索引,省去后续为了不越界的 -1 操作 */
    initZero(numsTimes, 2 * max + 1);
    
    for (i = 0; i < numsSize; i++){
        cur = nums[i];
        ++numsTimes[cur];
    }
    
    int *frequent = (int *)malloc(sizeof(int) * k);
    i = 0;
    while(i < k){
        maxi = maxIndex(numsTimes, 2*max+1);
        frequent[i++] = maxi - max; /* 需 -max, 对应于原 nums 中的值 */
        numsTimes[maxi] = 0;
    }

    return frequent;
}
一个更简洁的表达方式(但会多 o(k) 的时间)
int maxNumber(int *nums, int numsSize);
int maxIndex(int *nums, int numsSize);
void initZero(int *nums, int numsSize);
int* topKFrequent_pos(int* nums, int numsSize, int k);

 /* 是上面方法的汇总 */
int* topKFrequent(int* nums, int numsSize, int k){
    int max = abs(maxNumber(nums, numsSize));
    addVal(nums, numsSize, max);
    int *frequent = topKFrequent_pos(nums, numsSize, k);
    addVal(frequent, k, -max); /* 该语句为多余时间来源 */
    return frequent;
}

链表

节点定义

/* 链表节点结构体定义 */
struct Listnode{
    int val;
    struct Listnode *next;
};

基本操作·

/* 判空 */
int isempty(struct Listnode *node){
    return node == NULL;
}

/* 判断是否为尾指针,对于空指针返回 0 */
int islast(struct Listnode *node){
    if (!node && node->next == NULL)
    	return 1;
    else
        return 0;
}

/* 返回关键字首次出现的节点指针,若未找到返回 NULL */
struct Listnode *find(struct Listnode *head, int val){
    struct Listnode *ret = head;
    
    while (ret != NULL){
		if (ret->val == val)
            return ret;
        ret = ret->next;
    }
    return ret;
}

/* 插入节点(头插法),可实现链表逆置 */
void insert(struct Listnode *head, struct Listnode *node){
    node->next = head->next;
    head->next = node;
}

/* 删除下一节点 */
void deleteNext(struct Listnode *node){
	struct Listnode *t = node->next;
    node->next = t->next;
    free(t);
}

/* 删除当前节点(无法处理最后一个节点),删除成功返回1,否则(为最后一个节点)返回0 */
int deleteCur(struct Listnode *cur){
	/* 思路为用下一个节点覆盖当前节点,然后删除下一节点,达到删除当前节点的效果 */
    struct Listnode *next = cur->next;
    if (!isempty(next)){
        cur->val = next->val;
        cur->next = next->next;
        free(next);
        return 1;
    }
    return 0;
}

算法题

1. 反转链表( LeetCode 206 题)

解法1: 前后双指针

(无额外头结点,即判空的语句为 head == NULL)

算法思路:利用前后双指针p,q实现节点之间的反转,p 初始为 head ,q 始终为 p->next,即指向需翻转的下一个节点,故还需要一个节点 t 保存 q->next,在通过 q->next = p 翻转链表后,令 q = t,p = q 以移动指针处理下一节点,当 q 为尾指针即 t 为 NULL 时代表完成反转(以 q == NULL 为结束条件更好,否则 t 需要提前初始化,而且因为 q 在反转后指向 t 所指,故依然正确)。

首个节点需要对 *head 单独处理,否则会导致局部循环,处理也很简单,在初始化用 q 保存了 head-next 后,令head->next = NULL。

在初始化前需要判断链表是否为空,若为空直接返回 head。

struct ListNode *reverseList(struct ListNode* head){
    struct ListNode *p, *q, *t;

    if(head != NULL){
        p = head;
        q = p->next;
		head->next = NULL;
        
        while(q != NULL){
            t = q->next;
            q->next = p; /* 1.这一步为反转步骤,其余均是为了两个指针能够向后移动 */
            p = q;
            q = t;
        }
        head = p; /* 2.仅为了只写一个 return,也可以在这里直接 return p; */
    }
    return head;
}
解法2:递归

算法思路:假设链表有 n 个结点,链表可以分解为 1 个头结点和 n-1 个剩余结点两个部分,剩余结点组成的链表亦可继续分解,直到 1 个头结点和 0 个剩余结点,也即 head->next == NULL,这时便有了递归终止的条件,满足该条件时返回 head。

现在根据递归的特性,倒退回 1 个头结点和 1 个剩余结点的情况,反转只有两个节点的链表仅需要头结点的指针,此时令 head->next->next = head(该步和解法1的注释1所在行的目的一样), head->next = NULL 即可,这是基准情况。递归过程本身可以看成保存数据的栈,所以不需要像解法1那样额外记录下一个反转的结点,之后每次倒退其实都可以看成只有两个“结点”的链表,一个是头结点,一个是已经反转好的链表。

现在最后的需求是返回新的头结点,即原来链表的尾结点,有三种方式(其实想法一致):

  1. 定义一个全局变量 *rhead ,rhead 在触发递归终止条件时记录当前所处结点,随后一直返回 rhead(因为 head 会随递归出栈而变化,故记录)。
  2. 和 1 思路一样,不过是在函数内定义变量,然后将递归的步骤在一个小函数中重写,该函数接受的参数为(head, &rhead),这里 rhead 的声明和上面不同,是指向结点类型的指针的指针。
  3. 递归在终止时返回的就是反转后的 head,故只需要记录该指针并令其始终为返回值即可(当前递归程序并不需要额外的返回值来帮助反转链表)
/* 方式1 */
struct ListNode *rhead;

struct ListNode *reverseList(struct ListNode* head){
    if(head == NULL || head->next == NULL){
        rhead = head;
        return rhead;
    }

    reverseList(head->next);
    head->next->next = head;
    head->next = NULL;
    return rhead;
}

/* 方式3 */
struct ListNode *reverseList(struct ListNode* head){
 	if (head == NULL || head->next == NULL)
        return head;
    
    struct Listnode *rhead = reverseList(head->next);
    head->next->next = head; /* 反转 */
    head->next = NULL;
    return rhead;
}

2. 返回倒数第 k 个结点的值(LeetCode 面试题 02.02.)

解法1: 快慢指针

算法思路:初始化两个指针 p 和 q,p 先逐个遍历结点,用 i 记录 p 遍历的结点个数,当 p 遍历了 k-1 个结点后,q 开始和 p 一起逐步遍历,当 q == NULL时, q 恰好指向链表的倒数第 k 个结点。

int kthToLast(struct ListNode *head, int k){
    struct ListNode *p = head;
    struct ListNode *q = head;
    int i = 0;
    
    while (p != NULL){
        p = p->next;
        if (i > k-1) /* k-1 是因为 k>=1,而当 k==1时,q与p指向同一个结点(可以初始化 i=1,if(i >k)) */
            q = q->next;
        else 
            ++i; /* ++i 移到 if 语句前也可以,放在这里是因为当 i>k 后就没有必要再对 i 进行 +1 操作了 */
    }
    return q->val;
}
解法2: 递归

算法思路:初始化全局变量 i = 0,当递归至 NULL 时(若递归至尾结点,那么当 k == 1时还需额外判断,所以这里就改成 NULL),i++(或 i = 1),然后开始递归出栈,出栈时每次判断 i 是否等于 k,并令 i++,若等于,返回当前结点的值

int i = 0;

int kthToLast(struct ListNode *head, int k){
    if (head == NULL){
		i++; /* 在 LeetCode 中这里应改为i = 1(原因见 LeetCode 对全局变量的处理) */
        return -1;
    }
    
    int ret = kthToLast(head->next, k);
    if (i++ == k)
        return head->val;
    else 
        return ret;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hoper.J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值