作为初学者,总结了下自己写代码所踩过的坑,并做笔记记录下心得和总结
一,从变量开始
变量部分看似不起眼,但经常会产生一些隐蔽的问题。一般变量部分最容易产生如下问题:
1,类型使用错误,比如定义变量时总忘记类型是否正确
比如 int a=5; int b = a/2; 这时得到b的结果显然不是我们想要的结果,所以定义变量时,数据类型一定要注意。
注:再说个比较经典的问题
#include <iostream>
#include <stdio.h>
using namespace std;
int main()
{
float a = 0;
for(int i = 0; i < 100; i++)
{
a+=0.1;
}
printf("%f",a);
return 0;
}
结果为 10.000002 。是的,你没有看错!100个0.1相加确实不等于10,这是因为计算机底层是二进制表示数据的,十进制的0.1在二进制下就是0.0001100110011……(1100循环)这样的循环小数,而且计算次数越多,精度损失越大,所以这种隐蔽的问题有必要时刻记住
2,变量类型越界
这个也是个容易出错的地方,比方32位系统下 int能表示的最大值为 2^32 - 1 = 4294967295,而平常我们写程序很少会有这么大的值,所以写习惯后,非常容易忽略类型长度问题,导致程序出现异常,比如下面这个问题
质数筛(埃拉托斯特尼筛法) 求出1~100000中所有的素数
代码如下
const long long N = 100001;
//检查N是否为质数,0为质数,1为合数
int check[N] = {0};
vector<int> prim;
for (int i = 2; i < N; i++)
{
if (check[i] == 0)
{
prim.push_back(i);
for (long long j = i; j * i < N; j++) // <== 这里注意类型越界,int改为long long
{
check[i * j] = 1;
}
}
}
代码中指出了类型越界的隐患处,即 当 i == N时,j*i肯定是超过int 所能表示的范围了,所以我这里把它改成了long long 双整形,否则,这段程序将不能正确运行
总结:写代码一定要注意定义清晰,不仅在变量的定义上有这种问题,很多问题的产生其实就是定义不清晰而导致的,明确了定义时,很多问题也就迎刃而解。
二,注意边界的定义--数组问题
其实程序里面很多问题都是数组上的问题。所谓数组,就是有序的元素序列,既然有多个元素,那就肯定有边界问题,所以这里将讨论正确写出程序需要注意的一大问题--边界定义问题
这里用二分查找为例
int binarySearch(vector<int> &v, int target)
{
//定义范围[0......v.size()-1]
int r = v.size() - 1;
int l = 0;
int mid = (r + l) / 2;
while (l <= r)
{
if(v[mid] == target)
{
return mid;
}
if (target > v[mid])
{
l = mid + 1;
}
if (target < v[mid])
{
r = mid - 1;
}
mid = (r + l) / 2;
}
return -1;
}
二分查找的思想应该都比较熟悉,在一组有序的集合中,选择一个"标尺",然后待查找元素与这个标尺值作比较,比标尺大的在较大的区间去查找,比标尺小就去较小的区间去查找。根据这个定义,查找的范围一定要确定,得保证不会重复或者漏查元素的情况发生
我这段代码里,初始区间定义为了左右均闭合的区间,所以初始的 l 为 0, r 为 v.size()-1, 这样我们查找的范围就能保证所有元素都能查到
然后下面二分查找时,需要定义下一个查找的区间,那么我们定义的新区间肯定是不能包含其它区间的元素的,否则很可能会产生重复查找的现象,所以我每次进行判断时,如 target < v[mid] ,我的 右边界 r 取值为 mid-1,下一个区间并不包含查过的元素,所以说,明确了定义和范围,其实问题的解决方法也就清晰可见了
总结:明确边界和范围,减少思绪紊乱。边界问题不仅在算法问题上非常重要,在平常的业务代码上面也有很大用处,比如MVC模式中,定义好了Controller来解析参数,调用service层等,有人为了图省事,搞一些"越权"操作,如直接在Controller里面去调用dao等。个人觉得这个习惯不好,容易让边界和职责变得混乱,后期不利维护,所以养成明确边界的思想还是有必要的。
三,细节
上面说的都是些非常典型的案例,这里说说一般问题中怎么去看定义和划分边界的细节
这里用LeetCode上第21号问题为例,一个单链表的归并问题来说
注:其实熟悉归并排序的话,这个问题非常好解决,就是归并排序中的merge操作而已,这里讲讲我最开始学习归并的理解方法和经验
首先明确题目的定义,要完成的目标。这个问题的定义是,两个排序后的,链表进行合并;合并方式应该是通过拼接两个链表的方式返回
根据上面的信息可知:①链表是排好序的,所以问题的范围是"有序的序列"。②需要返回新列表。③合并方式是拼接两个链表的节点完成
首先看定义,定义两个有序的链表,合并成新链表
链表的
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
所以函数原型就应该是———新链表 mergeTwoLists(表1,表2);;
然后可以考虑先定义一个新链表,用用两个指针分别指向两个链表并向后移动,比较这两个指针指向节点的大小,较小节点的放入到新链表中,然后指向该节点的指针后移,继续进行比较,知道其中一个链表被遍历完
然后就是边界,问题要对哪些对象做处理,很显然是两个链表里的元素都要处理到,所以这里就能确认边界,保证不遗漏任何一种情况。所以 mergeTwoLists函数可以先写成这样:
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
{
ListNode tmp(0); //先定义一个新表头
ListNode *tail = &tmp; //用于遍历新表的指针
ListNode *p1 = l1;
ListNode *p2 = l2; //遍历两个目标表的指针
while (p1 != NULL && p2 != NULL) //判断条件
{
if (p1->val < p2->val)
{
tail->next = new ListNode(p1->val);
p1 = p1->next;
}
else
{
tail->next = new ListNode(p2->val);
p2 = p2->next;
}
tail = tail->next;
}
//记得边界内的对象不要遗漏
if(p1 == NULL){
tail->next = p2;
}
if(p2 == NULL){
tail->next = p1;
}
return tmp.next;
}
还有一点要注意的就是细心,由于操作的是链表,所以没有必要new新内存来储存,修改后应该是这样:
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
{
ListNode dummy(0);
ListNode *tail = &dummy;
while (l1 && l2)
{
if (l1->val < l2->val)
{
tail->next = l1;
l1 = l1->next;
}
else
{
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
tail->next = l1 ? l1 : l2;
return dummy.next;
}
四,多喝热水
总之,多写多改,多思考,多锻炼。
好的代码是在一定代码量的基础上积累起来的,写的时候要多加思考,不能不知道自己在干什么。