本人正在写矩阵史诗级玩法系列博客,写到求二元二次方程组的地方来了,消元后最高会生成一元四次方程,而这个求根公式虽然成熟,但代码量也不少,所以单独封装成工具类。
本不打算讲解的,但考虑到有的朋友可能没接触过复数,或者说虽然接触过复数但已经忘得一干二净,那这里我就简单说一下好了。在实数范围内,负数是没办法开平方的,而且对于一些简单的曲线求交(二次或以下)问题来说,出现要对负数开平方的话就意味着没有交点。然而在一些复杂的方程求解中,负数开平方也许是一个重要的中间过程,走完中间过程之后可能会得到实数根,所以数学家们定义了一个新的数i,名为虚数单位,并规定i^2=-1,同时可以使其参与到跟实数的四则运算中且交换律结合律等仍然成立。然后,实数1+实数2*i就构成了复数a+bi。
四次方程求解经常用到复数,其结构为实数部分一个系数(实部),虚数部分一个系数(虚部),代码如下。
function ComplexNum(real, image)
{
this.real = isNaN(real) ? 0 : real;
this.image = isNaN(image) ? 0 : image;
}
复数本身有四则运算,所以完整的代码我把这些运算的实现都写进去了,加减最好理解,乘法稍有难度,除法和开方需要套路,如果觉得看不懂,那可以自己百度或者查阅相关的高中数学教材来补补这方面的知识。
ComplexNum.js
function ComplexNum(real, image)
{
this.real = isNaN(real) ? 0 : real;
this.image = isNaN(image) ? 0 : image;
/**
* 设置复数的值
* @param reala 复数的实部
* @param imagea 复数的虚部
*
*/
function setTo(reala, imagea)
{
this.real = reala;
this.image = imagea;
}
this.setTo = setTo;
/**
* 复数相加
* @param complex 要与其相加的复数
* @return
*
*/
function add(complex)
{
return new ComplexNum(this.real + complex.real, this.image + complex.image);
}
this.add = add;
/**
* 为了使用上方便一点,此处加上与实数相加的方法
* @param num 要与其相加的数字
* @return
*
*/
function addNumber(num)
{
return new ComplexNum(this.real + num, this.image);
}
this.addNumber = addNumber;
/**
* 复数相减
* @param complex 要与其相减的复数
* @return
*
*/
function subtract(complex)
{
return new ComplexNum(this.real - complex.real, this.image - complex.image);
}
this.subtract = subtract;
/**
* 为了使用上方便一点,此处加上与实数相减的方法
* @param num 要与其相减的数字
* @return
*
*/
function subtractNumber(num)
{
return new ComplexNum(this.real - num, image);
}
this.subtractNumber = subtractNumber;
/**
* 复数乘法 (运算法则是利用多项式乘法进行展开得到)
* @param complexNum 要与其相乘的复数
* @return
*
*/
function multiply(complexNum)
{
return new ComplexNum(this.real * complexNum.real - this.image * complexNum.image, this.image * complexNum.real + this.real * complexNum.image);
}
this.multiply = multiply;
/**
* 为了使用上方便一点,此处加上与实数相乘的方法
* @param num 要与其相乘的数字
* @return
*
*/
function multiplyNumber(num)
{
return new ComplexNum(this.real * num, this.image * num);
}
this.multiplyNumber = multiplyNumber;
/**
* 复数除法 (运算法则是通过平方差公式,分子分母同时乘以分母的共轭复数以去除分母中的虚部,然后就利用乘法法则进行计算)
* @param complexNum 要与其相除的复数
* @return
*
*/
function divide(n)
{
//分母化为实数
var denominator = n.real * n.real + n.image * n.image;
//分子也乘以同样的复数,并除以分母即得最终结果
return new ComplexNum((this.real * n.real + this.image * n.image) / denominator, (this.image * n.real - this.real * n.image) / denominator);
}
this.divide = divide;
/**
* 为了使用上方便一点,此处加上与实数相除的方法
* @param num 要与其相除的数字
* @return
*
*/
function divideNumber(num)
{
return new ComplexNum(this.real / num, this.image / num);
}
this.divideNumber = divideNumber;
/**
* 开平方运算
* @return
*
*/
function squareRoot()
{
return this.getRoot(2);
}
this.squareRoot = squareRoot;
/**
* 开立方运算
* @return
*
*/
function cubicRoot()
{
return this.getRoot(3);
}
this.cubicRoot = cubicRoot;
/**
* 开任意整数次方的运算
* @param times
* @return
*
*/
function getRoot(times)
{
var vec = [];
//复数开方运算的原理是把辐角根据次数进行平分
var degree = this.degree;
degree /= times;
//然后多个方根平分360度,所以需要算出每个方根之间的辐角间隔
var degreeUnit = 360 / times;
//复数长度(模)直接开方即可
var lengthRoot = Math.pow(this.length, 1 / times);
var cosDic = AngleUtil.getCosDic();
var sinDic = AngleUtil.getSinDic();
//然后就能通过循环生成所有开方结果
for(var i = 0; i < times; i ++)
{
var currentDegree = (degree + i * degreeUnit);
var currentAngle = currentDegree * Math.PI / 180;
var cos = isNaN(cosDic[currentDegree]) ? Math.cos(currentAngle) : cosDic[currentDegree];
var sin = isNaN(sinDic[currentDegree]) ? Math.sin(currentAngle) : sinDic[currentDegree];
//trace(lengthRoot * cos, lengthRoot * sin);
vec.push(new ComplexNum(lengthRoot * cos, lengthRoot * sin));
}
return vec;
}
this.getRoot = getRoot;
/**
* 复数的辐角主值(复数所在点与坐标连线跟水平线的夹角),以弧度为单位
* 必要时可用degree属性可以更有效避免边缘位置的浮点误差引发的错误
* @see degree()
*
*/
Object.defineProperty(this, "angle", {get: getAngle});
function getAngle()
{
//由于复数开方基于角度,所以一旦有浮点误差就会导致角度在边缘位置出现突跃导致严重错误,所以遇到浮点误差的话,我会强制将角度设置为0
if(Math.abs(this.image) < 0.0000001)
{
return this.real > 0 ? 0 : Math.PI;
}else if(Math.abs(this.real) < 0.0000001)
{
return this.image > 0 ? Math.PI * 0.5 : (this.image == 0 ? 0 : - Math.PI * 0.5);
}else
{
return Math.atan2(this.image, this.real);
}
}
/**
* 复数的辐角主值,以角度值表示(跟弧度相比,该方法能更有效地避免边缘位置的浮点误差引发的错误)
*
*/
Object.defineProperty(this, "degree", {get: getDegree});
function getDegree()
{
//由于复数开方基于角度,所以一旦有浮点误差就会导致角度在边缘位置出现突跃导致严重错误,所以遇到浮点误差的话,我会强制将角度设置为0
if(Math.abs(this.image) < 0.0000001)
{
return this.real > 0 ? 0 : 180;
}else if(Math.abs(this.real) < 0.0000001)
{
return this.image > 0 ? 90 : (this.image == 0 ? 0 : 270);
}else
{
return Math.atan2(this.image, this.real) / Math.PI * 180;
}
}
/**
* 复数的模(长度)
* @return
*
*/
Object.defineProperty(this, "length", {get: getLength});
function getLength()
{
return Math.sqrt(this.real * this.real + this.image * this.image);
}
/**
* 返回当前复数的一个副本
* @return
*
*/
function clone()
{
return new ComplexNum(this.real, this.image);
}
this.clone = clone;
function toString()
{
var realStr = String((this.real != 0 || this.image == 0) ? real : "");
var imageStr = (this.image == 0) ? "" : ((this.image < 0 ? ("-" + (-this.image)) : ((realStr == "" ? "" : "+") + this.image)) + "i");
return realStr + imageStr;
}
this.toString = toString;
}
复数乘法跟三角函数密切相关,而且60度倍数的三角函数出现频率特别高,所以我又定义了一些常量,以防直接计算累积不必要的浮点误差。
ComplexConsts.js
function ComplexConsts()
{
}
/**
* OMEGA是模为1,幅角主值等于120度的复数,它是1开三次方的结果,在解3次和4次方程中非常常用
* OMEGA就是希腊字母里最像w的那个
*
*/
ComplexConsts.OMEGA = new ComplexNum(1 / 2, Math.sqrt(3) / 2);
/**
* 理论上OMEGA的平方可以用两个OMEGA相乘得到,但由于容易产生浮点误差,加上数值固定,因此也直接做成常量
*
*/
ComplexConsts.OMEGA_SQUARE = new ComplexNum(1 / 2, Math.sqrt(3) / 2);
/**
* OMEGA的3次方恰好等于实数1,就最好不要再自己用三个OMEGA来相乘了
*
*/
ComplexConsts.OMEGA_CUBIC = new ComplexNum(1, 0);
/**
* OMEGA的0次方,纯属为了让代码清晰而定义的常量
*
*/
ComplexConsts.OMEGA_ZERO = new ComplexNum(1, 0);
/**
* 虚数单位i
*
*/
ComplexConsts.I = new ComplexNum(0, 1);
AngleUtil.js
function AngleUtil()
{
}
AngleUtil.getCosDic = function()
{
if(AngleUtil._cosDic == undefined)
{
AngleUtil._cosDic = [];
AngleUtil._cosDic[60] = AngleUtil._cosDic[-60] = AngleUtil._cosDic[300] = AngleUtil._cosDic[-300] = 0.5;
AngleUtil._cosDic[120] = AngleUtil._cosDic[-120] = AngleUtil._cosDic[240] = AngleUtil._cosDic[-240] = -0.5;
AngleUtil._cosDic[0] = AngleUtil._cosDic[360] = 1;
AngleUtil._cosDic[180] = AngleUtil._cosDic[-180] = -1;
AngleUtil._cosDic[90] = AngleUtil._cosDic[270] = AngleUtil._cosDic[-90] = AngleUtil._cosDic[-270] = 0;
}
return AngleUtil._cosDic;
}
AngleUtil.getSinDic = function()
{
if(AngleUtil._sinDic == undefined)
{
AngleUtil._sinDic = [];
AngleUtil._sinDic[30] = AngleUtil._sinDic[150] = AngleUtil._sinDic[-330] = AngleUtil._sinDic[-210] = 0.5;
AngleUtil._sinDic[-30] = AngleUtil._sinDic[-150] = AngleUtil._sinDic[210] = AngleUtil._sinDic[330] = -0.5;
AngleUtil._sinDic[90] = AngleUtil._sinDic[-270] = 1;
AngleUtil._sinDic[270] = AngleUtil._sinDic[-90] = -1;
AngleUtil._sinDic[180] = AngleUtil._sinDic[-180] = AngleUtil._sinDic[0] = AngleUtil._sinDic[360] = 0;
}
return AngleUtil._sinDic;
}
真没想到,弄完这些辅助类就把文章撑那么长了。那我就分开两篇写好了。
其实我可以弄附件,但有些东西没测好,附件又似乎不太好改,那就还是直接贴吧。
下篇我贴出解方程的核心类。