如何正确地写出代码(一)

作为初学者,总结了下自己写代码所踩过的坑,并做笔记记录下心得和总结

一,从变量开始

变量部分看似不起眼,但经常会产生一些隐蔽的问题。一般变量部分最容易产生如下问题:

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;
}
            

四,多喝热水       

        总之,多写多改,多思考,多锻炼。

        好的代码是在一定代码量的基础上积累起来的,写的时候要多加思考,不能不知道自己在干什么。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值